Créer et envoyer un email

publié le 16/08/2010 par Matthieu Sadouni

Ruby on Rails utilise ActionMailer pour la gestion des emails. Voyons comment créer et envoyer un email au format texte et HTML, ainsi que quelques astuces pour en simplifier la gestion.

Principe

Le framework Ruby on Rails est construit sur le principe MVC, et les emails n’échappent pas à cette règle. Ils sont gérés par des modèles un peu particuliers qui héritent de ActionMailer::Base, les Mailer. Chaque Mailer concerne une partie de l’application, par exemple :

  • UserMailer pour les emails en rapport avec l’inscription et l’activation de compte
  • PostMailer pour les emails de notification d’un nouvel article aux abonnés
  • OrderMailer pour les emails de notification des différentes étapes d’avancement d’une commande

Dans chaque Mailer, une méthode de classe est définie pour chaque email. Y sont définis le destinataire, le sujet, les variables utilisées dans le corps du message, etc. Le message final est compilé à partir d’une vue, comme n’importe quelle action de contrôleur dans l’application.

Création d’un Mailer

Un script fourni avec Ruby on Rails permet de générer les fichiers nécessaires :

    $ script/generate mailer user_mailer
    

Intéressons-nous tout d’abord au fichier models/user_mailer.rb, auquel nous ajoutons une méthode représentant un email de bienvenue après l’inscription :

# app/models/user_mailer.rb
    class UserMailer < ActionMailer::Base
      def bienvenue(user)
        recipients user.email
        from "Example.com<no-reply@example.com>"
        subject "Bienvenue !"
        body :user => user, :account_url => edit_user_url(user)
      end
    end
    
  • recipients définit le destinataire à partir d’une chaîne contenant une adresse email, ou un tableau de chaînes (sous forme ['email1@example.com', 'John<john@example.com>])
  • from définit l’adresse de l’expéditeur
  • subject définit, sans surprise, le sujet
  • body permet de définir des variables accessibles dans la vue

La liste complète des attributs est disponible dans le chapitre 2.3 du guide ActionMailer (en anglais). Les plus utilisés, à part ceux présentés ici, sont probablement bcc et reply-to.

Nous voyons ici que l’une des variables accessibles dans la vue, account_url, contient l’url du compte de l’utilisateur. Les Mailers n’ayant pas accès au contexte de la requête, il faut paramétrer l’URL de base de l’application pour que le lien complet puisse être construit. Nous ajoutons pour cela une ligne au fichier de l’environnement concerné :

# config/environments/<environment>.rb
    config.action_mailer.default_url_options = {:host => "example.com"}
    

Avec pour :host le nom du domaine sur lequel est hébergé l’application. En développement, ce sera par exemple localhost:3000.

Il ne nous reste plus qu’à créer la vue correspondante :

# app/views/user_mailer/bienvenue.text.plain.erb

    Bienvenue <%= @user %> !
    Votre compte : <%= @account_url %>
    

Notons que le nom du fichier mentionne le type de format utilisé (text.plain). Nous y reviendrons juste après le chapitre suivant.

Envoi d’un email

Une fois l’email créé, le plus simple pour l’envoyer est d’appeler la méthode correspondante dans un contrôleur :

# app/controllers/users_controller.rb
    class UsersController < ApplicationController
      def create
        @user = User.new(params[:user])
        if @user.save
          UserMailer.deliver_bienvenue(@user)
          redirect_to root_path and return
        end
      end
    end
    

Nous remarquons que la méthode appelée est deliver_bienvenue et non bienvenue. ActionMailer utilise method_missing (en savoir plus sur method_missing - en anglais) pour retrouver la méthode demandée bienvenue et envoyer l’email. Il est ainsi également possible de créer notre email sans l’envoyer, via create_bienvenue. Ceci permet de n’écrire qu’une seule méthode pour définir un email, et de l’envoyer en une seule ligne de code. De plus, le code dans le contrôleur est plus lisible car l’intention est claire :

    UserMailer.deliver_bienvenue(user)
    # est plus explicite que
    UserMailer.bienvenue(user)
    

Il existe deux autres manières d’envoyer un email. La première, depuis un modèle, par exemple ici dans la méthode after_create :

# app/models/user.rb
    class User < ActiveRecord::Base
      after_create :notify_by_email

      private
        def notify_by_email
          UserMailer.deliver_bienvenue(self)
        end
    end
    

Le souci avec cette approche est que du code sans rapport avec la responsabilité première du modèle lui est rajouté. Pour éviter cela, nous pouvons utiliser un Observer. Comme son nom l’indique, un Observer “observe” une classe et répond aux méthodes de rappel (callbacks) comme after_create, before_save etc. Pour mettre en place ce fonctionnement, nous commençons par créer un fichier app/models/user_observer.rb :

# app/models/user_observer.rb
    class UserObserver < ActiveRecord::Observer
      def after_save(user)
        UserMailer.deliver_bienvenue(user)
      end
    end
    

Pour que l’Observer soit chargé automatiquement au lancement de l’application, il faut le spécifier dans config/environment.rb :

# config/environment.rb
    Rails::Initializer.run do |config|
      ...
      config.active_record.observers = :user_observer
    end
    

Il ne reste plus ensuite qu’à supprimer de UsersController la ligne concernant l’envoi de l’email, celui-ci étant envoyé automatiquement par l’Observer après la création de l’utilisateur :

# app/controllers/users_controller.rb
    class UsersController < ApplicationController
      def create
        @user = User.new(params[:user])
        if @user.save
          redirect_to root_path and return
        end
      end
    end
    

Chaque méthode a ses avantages et ses inconvénients. Un Observer permet de garder le code du modèle et du contrôleur le plus simple possible, et de s’assurer que l’email est envoyé à chaque fois qu’un utilisateur est créé, même sans passer par le contrôleur (en console par exemple). Par contre, si l’application grossit et compte plusieurs observers réalisant chacun plusieurs tâches, il peut être difficile de bien voir tout ce qu’il se passe sur une action donnée.

L’envoi depuis le contrôleur ou un modèle permet quant à lui de garder un code plus clair, car chaque action effectuée est directement visible, en contrepartie de la possibilité d’une duplication de code. C’est à chacun de choisir la méthode appropriée selon ses besoins, en gardant à l’esprit que les deux approches sont compatibles.

Envoi au format HTML

L’envoi au format HTML est très simple : il suffit d’avoir deux vues du même nom que l’email, une pour la version texte et une pour la version HTML :

# app/views/user_mailer/bienvenue.text.plain.erb

    Bienvenue <%= @user %> !
    
    Votre compte : <%= @account_url %>
    

# app/views/user_mailer/bienvenue.text.html.erb

    <h1>Bienvenue <%= @user %> !</h1>
    
    <p><a href="<%= @account_url %>">Votre compte</a></p>
    

Rails va alors automatiquement construire un email au format multipart, sans aucune autre action nécessaire de notre part.

Utiliser un layout

Comme un contrôleur, un Mailer peut également utiliser un layout pour ses vues. Il suffit pour cela de créer un fichier dont le nom se termine par _mailer et nommé comme le Mailer. Pour UserMailer, les layouts app/views/layouts/user_mailer.text.plain.erb et app/views/layouts/user_mailer.text.html.erb seront automatiquement utilisés. Pour spécifier un autre fichier, il faut utiliser la méthode layout :

# app/models/user_mailer.rb
    class UserMailer < ActionMailer::Base
      layout 'application_mailer'
    end
    
# app/views/layouts/application_mailer.text.plain.erb
    <%= yield %>
    --
    À bientôt sur notre site
    Toute l'équipe de http://www.example.com
    

Le layout HTML, nécessaire si une vue existe au format HTML, serait quant à lui nommé application_mailer.text.html.erb.

Optimiser ses Mailers

À partir du moment où l’application utilise plusieurs Mailers, du code se retrouve dupliqué lors du paramétrage des emails :

  • les attributs from et sent_on sont quasiment toujours identiques
  • si un seul layout est utilisé pour tous les Mailers, il est nécessaire de le définir dans chacun puisque le fonctionnement automatique en fonction du nom du Mailer n’est plus possible
  • si nous souhaitons ajouter le nom de l’application entre crochets devant le sujet de chaque message pour faciliter le filtrage des emails, il faudrait le répéter dans chaque méthode de nos Mailers

Pour remédier à cela, nous créons une classe ApplicationMailer dont héritent les Mailers de l’application. Nous y centralisons le code commun dans une méthode setup et allégeons ainsi nos Mailers :

# app/models/application_mailer.rb
    class ApplicationMailer < ActionMailer::Base
      layout 'application_mailer'

      protected
        def setup(*args)
          from "Example.com<no-reply@example.com>"
          subject "[Example.com] "
          sent_on Time.now
        end
    end
    

Il nous suffit ensuite de faire hériter UserMailer de ApplicationMailer et d’appeler la méthode setup :

# app/models/user_mailer.rb
    class UserMailer < ApplicationMailer
      def bienvenue(user)
        setup
        recipients user.email
        @subject += "Bienvenue !"
        body :user => user, :account_url => edit_user_url(user)
      end
    end
    

Notons l’utilisation de @subject pour ajouter au sujet prédéfini dans setup le sujet de l’email.

Une autre forme de duplication apparaît également dans chaque Mailer, selon sa responsabilité. Par exemple, les emails de notre UserMailer sont presque toujours envoyé à un utilisateur passé en paramètre. Celui-ci est également passé à leur vue respective pour y afficher son nom, etc. Pour centraliser cela, nous surchargeons la méthode setup dans chaque Mailer concerné :

# app/models/user_mailer.rb
    class UserMailer < ApplicationMailer
      ...
      def bienvenue(user)
        setup(user)
        @subject += "Bienvenue !"
        body[:account_url] = edit_user_url(user)
      end

      def anniversaire(user)
        setup(user)
        @subject += "Joyeux anniversaire !"
      end

      def anonyme
        setup
        recipients ["test@example.com", "test2@example.com"]
        @subject += "Un simple exemple n'utilisant pas user"
      end

      protected
        def setup(user = nil)
          super
          if user
            recipients user.email
            body :user => user
          end
        end
    end
    

Notons l’utilisation de body[:account_url] pour ajouter une variable disponible dans la vue.

Nous venons de voir les bases de l’utilisation d’ActionMailer, couvrant les principaux besoins d’une application web en terme de création et d’envoi d’emails. ActionMailer propose également d’autres fonctionnalités comme la réception d’emails ou l’attachement de pièces jointes sur lesquelles nous reviendrons dans un prochain article.

blog comments powered by Disqus