Drupal  8 : l'envoi de mails sous toutes les coutures

Des mouettes et des pigeons sur la rambarde d'un pont

Par défaut, tous les mails envoyés par Drupal 8 le sont en texte brut. Pour envoyer des mails en HTML, que ce soit pour des newsletters, ou encore tout simplement pour les différentes notifications afin de les enrichir quelque peu, il est nécessaire de modifier le système d'envoi par défaut des courriers électroniques, ou encore d'en utiliser un autre. Le moyen le plus simple est d'utiliser un des modules permettant l'envoi de mails HTML, tels que SwiftMailer, HTMLMail ou encore MimeMail pour ne citer que les plus connus (tous ne disposent pas encore d'une version stable Drupal 8 à l'heure actuelle).

Ces modules ont aussi pour particularité d'être basé sur le Module Mail System qui offre une interface graphique permettant de modifier simplement le système d'envoi des mails et utiliser tel ou tel module pour assurer ou bien le formatage des courriers ou bien leur envoi. Ce module permet de spécifier également plusieurs interfaces chargées de l'envoi de mails en fonction du module responsable de l'envoi, ou encore de la clé utilisée pour générer un mail, ou bien un mixte des deux.

Par contre ces modules peuvent devenir limitant si nous souhaitons envoyer des mails de façon programmatique, afin de récupérer plus facilement un certain nombre de données par exemple, ou, lors de l'envoi d'un grand nombre de courriers, si nous souhaitons utiliser le système de Cron pour limiter et répartir la charge lors de ces envois.

Faisons un tour d'horizon du système d'envoi des courriers électroniques de Drupal 8. Cela est utile à plus d'un titre. Bien sûr si nous avons besoin de réaliser un envoi programmatique, mais aussi pour mieux comprendre le mécanisme général et donc le fonctionnement des différents modules disponibles pour réaliser cette tache.

Schéma général

L'envoi d'un mail avec Drupal 8 est assez similaire à celui en place avec Drupal 7.

Il se réalise en 6 étapes principales.

Schéma général d'envoi d'un mail avec Drupal 8

Etape 1 : Déclenchement de l' envoi du mail avec le service MailManager.

Nous déclenchons l'envoi d'un mail avec le service MailManager et sa méthode mail().

\Drupal::service('plugin.manager.mail')->mail()

Etape 2 : Préparation du mail

Le HOOK hook_mail() permet de préparer le contenu du message en fonction des différents paramètres passés au service MailManager. C'est à cette étape également que nous pouvons préparer les différents headers du mail sortant, dont par exemple le Content-Type.

Etape 3 : Altération possible du mail

Tout module peut altérer un mail avant qu'il soit transmis, voir annuler son envoi, en implémentant hook_mail_alter().

Etape 4 : Sélection du plugin responsable de l'envoi du mail. Par défaut le Plugin PHPMail est utilisé pour tous les mails émis.

Drupal 8 peut disposer de plusieurs interfaces chargées de l'envoi des mails. Nous pouvons par exemple avoir une interface pour l'envoi de mails avec Mandrill, ou SwiftMailer, ou la fonction native de PHP, etc. C'est à cette étape que le Plugin responsable de l'envoi du mail est sélectionné, sur la base des paramètres $module et $key passés au service MailManager à l'étape 1.

Etape 5 : Le Plugin réalise le formatage du mail

La mise en forme du champ body du mail est réalisée par la méthode format() de l'interface MailInterface. C'est à cet étape que le corps du mail est formatté pour être rendu soit en texte brut, soit au format HTML.

Etape 6 : Le Plugin procède à l'envoi du mail

Le mail est envoyé en utilisant le service implémenté par la méthode mail() de l'interface MailInterface.

Cette dernière méthode retourne le résultat du mail envoyé, ou en cas d'impossibilité d'envoi, une erreur.

 

Regardons un peu plus en détail chaque phase du cheminement d'un mail envoyé par Drupal 8. Et profitons-en pour réaliser notre envoi de mail au format HTML et non plus au format texte brut par défaut.

Le déclenchement de l'envoi du mail

Le déclenchement de l'envoi d'un mail peut être réalisé de multiples manières. Par des modules implémentant des notifications mail (Contact, Simple News, etc.), avec Rules, ou encore de façon programmatique. C'est cette dernière méthode que nous allons explorer plus en détail.

$sitename = \Drupal::config('system.site')->get('name');
$langcode = \Drupal::config('system.site')->get('langcode');
$module = 'my_module';
$key = 'my_key';
$to = $account->getEmail();
$reply = NULL;
$send = TRUE;

$params['message'] = t('Your wonderful message about @sitename', array('@sitename' => $sitename));
$params['subject'] = t('Message subject');
$params['options']['username'] = $account->getUsername();
$params['options']['title'] = t('Your wonderful title');
$params['options']['footer'] = t('Your wonderful footer');

$mailManager = \Drupal::service('plugin.manager.mail');
$mailManager->mail($module, $key, $to, $langcode, $params, $reply, $send);

L'envoi d'un mail est réalisé avec le service plugin.manager.mail avec sa méthode mail(). Cette méthode requiert quelques paramètres obligatoires :

  • le paramètre $module qui servira ultérieurement à la sélection du Plugin responsable de l'envoi du mail. Dans le cas de mails envoyés avec des modules contribués, ce paramètre est renseigné par le module qui a réalisé l'envoi.
  • Le paramètre $key qui va permettre de cibler précisément ce mail dans sa phase de préparation ou d'altération. Ce paramètre peut aussi être utilisé dans la sélection du Plugin (cf. Etape 4)
  • Le paramètre $to qui est le destinataire du mail
  • Le paramètre $langcode correspond à la langue dans lequel le mail doit être envoyé. Nous prenons ici la langue courante du site. Il pourrait être plus pertinent d'utiliser la langue par défaut sélectionnée par l'utilisateur depuis son compte.
  • Le tableau $params peut contenir autant de données que nécessaires. En règle générale il contient à minima le corps du mail (ici dans la variable $params['message']
  • Les deux derniers paramètres $reply et $send permettent de spécifier respectivement l'adresse mail de réponse, et de demander l'envoi du mail. Ce dernier paramètre $send peut par exemple être utilisé dans la phase d'altération (Etape 3)  pour annuler l'envoi du mail sous certaines conditions.

Dans notre exemple ci-dessus nous définissons quelques paramètres supplémentaires avec $params['options'] pour enrichir notre mail. Ces paramètres sont tout à fait optionnels, et c'est aussi une des facilités offertes par un envoi programmatique : associer au mail un ensemble de données complémentaires qui pourront être utilisées pour construire son corps de texte.

La préparation du mail

Le contenu du mail, ainsi que ses entêtes, est préparé durant cette phase, en implémentant hook_mail().

/**
 * Implements hook_mail().
 */
function my_module_mail($key, &$message, $params) {
  switch ($key) {
    case 'my_key':
      $message['from'] = \Drupal::config('system.site')->get('mail');
      $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
      $message['subject'] = $params['subject'];
      $message['body'][] = $params['message'];
      $message['options'] = [];
      if (isset($params['options']) && !empty($params['options'])) {
        foreach ($params['options'] as $key => $value) {
          $message['options'][$key] = $value;
        }
      }
      break;
  }
}

Nous préparons ici le mail et lui attribuons une adresse d'expéditeur, un Content-Type pour spécifier qu'il s'agit d'un mail au format HTML, et renseignons les 2 principaux contenus d'un mail : son objet et son corps de texte.

Nous récupérons également les différents paramètres passés en option pour les affecter à la variable $message qui sera transmise au Plugin chargé de son formatage et de son envoi. Nous pourrions également dans cette phase renseigner également les entêtes du mail pour par exemple modifier l'adresse mail de réponse si nous souhaitons qu'elle soit différente de celle de l'expéditeur.

L'altération du mail

Tout module peut ici altérer le contenu d'un mail, voire annuler son envoi, en implémentant hook_mail_alter(), avant qu'il ne soit transmis au Plugin pour formatage et envoi. Par exemple :

/**
 * Implements hook_mail_alter().
 */
function another_module_mail_alter(&$message) {
  switch ($message['key']) {
    case 'my_key':
      if ( /* Some conditions. */ ) {
        $message['body'][] = t('Additionnal message');
      }
      if ( /* Another condition. */ ) {
        $message['send'] = FALSE;
      }
      break;
  }
}

Nous pouvons ici rajouter du contenu au corps du mail, ou encore sous certaines conditions annuler l'envoi de ce dernier, modifier ses entêtes, etc.

La sélection du Plugin

Le formatage et l'envoi du mail proprement dit est assuré par un Plugin de Drupal 8, implémentant MailInterface. Par défault le seul Plugin disponible est PHPMail qui permet d'envoyer des mails au format texte brut.

Pour ajouter un nouveau Plugin, il nous faut le déclarer dans la configuration system.mail de notre site. Cette déclaration doit être réalisée au moment de l'installation de notre module. Il nous faut donc utiliser hook_install(), dans le fichier my_module.install de notre module.

/**
 * Implements hook_install().
 */
function my_module_install() {
  $config = \Drupal::configFactory()->getEditable('system.mail');
  $mail_plugins = $config->get('interface');
  if (in_array('my_module', array_keys($mail_plugins))) {
    return;
  }

  $mail_plugins['my_module'] = 'my_module_mail';
  $config->set('interface', $mail_plugins)->save();
}

Que faisons-nous ? Nous chargeons la configuration des interfaces Mail de notre site, et rajoutons au tableau $mail_plugins une valeur sous la forme $mail_plugin['key'] = 'value'.

La sélection du Plugin adéquat pour envoyer un mail va s'effectuer sur la base de la clé key ajoutée à notre configuration (dans notre exemple my_module). Cette clé peut avoir pour valeur soit le nom du module, soit le nom d'une clé (utilisée pour le déclenchement de l'envoi d'un mail, my_key dans notre exemple à l'étape 1), soit une concaténation des deux.

Ainsi, lors de l'étape 1, lorsque nous déclenchons l'envoi d'un mail, en spécifiant les paramètres $module et $key, nous ciblons alors un Plugin particulier à utiliser. Si aucune valeur correspondante n'est trouvée dans la configuration des interfaces, alors c'est le Plugin par défaut qui est utilisé.

La valeur affectée value à notre clé correspond à l'identifiant du Plugin Mail qui devra être chargé pour procéder au formatage et à l'envoi du mail.

Dernier point très important. Il est impératif de supprimer notre Plugin Mail de la configuration des interfaces Mail de Drupal 8 si le module est désinstallé. Pour ce il nous suffit d'implementer hook_unistall().  

/**
 * Implements hook_uninstall().
 */
function my_module_uninstall() {
  $config = \Drupal::configFactory()->getEditable('system.mail');
  $mail_plugins = $config->get('interface');
  if (!in_array('my_module', array_keys($mail_plugins))) {
    return;
  }

  unset($mail_plugins['my_module']);
  $config->set('interface', $mail_plugins)->save();
}

Il ne nous reste plus qu'à créer notre Plugin Mail pour assurer l'envoi proprement dit, nous permettant de maîtriser l'ensemble de la chaîne d'envoi.

La création du Plugin Mail

Pour créer notre Plugin, rien de plus simple. Il nous suffit de créer un fichier sous l'arborescence src/Plugin/Mail/MyModuleMail.php de notre module.

**
 * @file
 * Contains \Drupal\my_module\Plugin\Mail\MyModuleMail.
 */
namespace Drupal\my_module\Plugin\Mail;

use Drupal\Core\Mail\MailInterface;
use Drupal\Core\Mail\Plugin\Mail\PhpMail;
use Drupal\Core\Render\Markup;
use Drupal\Core\Site\Settings;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Renderer;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;

/**
 * Defines the plugin Mail.
 *
 * @Mail(
 *   id = "my_module_mail",
 *   label = @Translation("My Module HTML mailer"),
 *   description = @Translation("Sends an HTML email")
 * )
 */
class MyModuleMail extends PHPMail implements MailInterface, ContainerFactoryPluginInterface {

  /**
   * @var \Drupal\Core\Render\Renderer;
   */
  protected $renderer;

  /**
   * MyModuleMail constructor.
   *
   * @param \Drupal\Core\Render\Renderer $renderer
   *   The service renderer.
   */
  function __construct(Renderer $renderer) {
    $this->renderer = $renderer;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $container->get('renderer')
    );
  }

Vous noterez l'identifiant du Plugin déclaré dans les annotations du Plugin. C'est ce même identifiant que nous avons déclaré dans la configuration des interfaces Mail à l'étape précédente.

Pour notre cas de figure nous étendons le Plugin existant PHPMail pour utiliser sa méthode d'envoi, celle native à PHP. Nous aurons besoin de surcharger uniquement le formatage du mail pour rendre celui-ci au format HTML. Si nous souhaitons utiliser une autre méthode d'envoi, il n'est bien sûr pas nécessaire d'étendre PHPMail. 

A noter aussi, nous récupérons le service Renderer par injection de dépendance. C'est la raison pour laquelle notre Plugin implémente ContainerFactoryPluginInterface

Passons maintenant à ce qui nous intéresse en premier lieu, le formatage de notre mail.

Le formatage du mail

Cette phase nous permet de construire, à proprement parler, le contenu intégral du corps de texte du mail. Jusqu'à présent nous avons passé en paramètre aux différentes fonctions appelées des éléments sur son contenu. Il ne reste plus qu'à le mettre en forme, et essayer d'obtenir un rendu consistant sur tous les clients de messagerie électronique.

Nous créons donc la fonction format() au sein de notre Plugin, qui pourrait ressembler à cela.

/**
 * {@inheritdoc}
 */
public function format(array $message) {
  
  $message = $this->cleanBody($message);
  $message['options']['texte'] = $message['body'];

  $render = [
    '#theme' => 'my_module_courriel',
    '#message' => $message,
  ];
  $message['body'] = $this->renderer->renderRoot($render);
  return $message;
}

Le but de cette méthode est de transmettre tous les éléments de contenu à un template Twig qui va nous permettre de construire tout le rendu HTML du corps de notre mail.

Nous utilisons en premier lieu une méthode interne à notre Plugin pour transformer les différents textes ajoutés à la variable $message['body'] en tableau pouvant être rendu (renderable arrays), et désinfecté le cas échéant, par le moteur de template Twig. 

Nous affectons ce texte, désormais sain (i.e. n'offrant pas de vecteurs d'attaque XSS par exemple), à la variable $message['options']['texte'].

Puis nous utilisons le service Renderer pour construire la structure finale du HTML à inclure dans le corps du mail, en utilisant donc le template my_module_courriel, structure finale que nous ré-affectons à notre variable $message['body'] qui va être envoyée en tant que corps du mail.

Cela suppose que nous ayons au préalable implémenté hook_theme pour déclarer notre thème.

/**
 * Implements hook_theme().
 */
function my_module_theme($existing, $type, $theme, $path) {
  return [
    'my_module_courriel' => [
      'template' => 'mail',
      'variables' => [
        'message' => array(),
      ],
    ],
  ];
}

Nous pouvons aussi déclarer une fonction de preprocess pour notre template my_module_mail déclaré, afin de préparer quelque peu les variables passées, voire pour une dernière fois les altérer.

function template_preprocess_my_module_courriel(&$variables) {
  if (isset($variables['message']['options']) && !empty($variables['message']['option'])) {
    foreach ($variables['message']['options'] as $key => $value) {
      $variables[$key] = $value;
    }
  }
}

Et il ne reste plus qu'à utiliser un template Twig, qui se chargera au passage d'assurer un rendu sain grâce à la désinfection des éléments activée par défaut. Nous pouvons utiliser alors un des nombreux templates responsives de mail disponibles sur l'Internet. Le template ci-dessous va par exemple afficher les variables username, title, footer passés en paramètre à l'étape 1, et bien entendu le texte principal passée à la variable texte.

{# File mail.html.twig #}
<!doctype html>
<html>
<head>
  <meta name="viewport" content="width=device-width">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>{{ title }}</title>
</head>

<body>

<!-- body -->
<table>
  <tr>
    <td>
      <!-- content -->
      <div class="content">
        <table>
          <tr>
            <td>
              {% if title %}
                <h1>{{ title }}</h1>
              {% endif %}

              {% if username %}
                <p>Bonjour {{ username }},</p>
              {% else %}
                <p>Bonjour,</p>
              {% endif %}

              {% if texte %}
                <p>{{ texte }}</p>
              {% endif %}

            </td>
          </tr>
        </table>
      </div>
      <!-- /content -->
    </td>
  </tr>
</table>
<!-- /body -->

{% if footer %}
<!-- footer -->
<table class="footer-wrap">
  <tr>
    <td class="container">
      <!-- content -->
      <div class="content">
        <table>
          <tr>
            <td align="center">
              <p>{{ footer }}</p>
            </td>
          </tr>
        </table>
      </div>
      <!-- /content -->
    </td>
  </tr>
</table>
<!-- /footer -->
{% endif %}

</body>
</html>

Vous pouvez alors contôler très finement quels contenus seront rendus et dans quelle zone de votre mail. Vous remarquerez l'utilisation extensive de l'élément table. Je crois que l'on a pas encore mieux en 2016 pour s'assurer d'un rendu homogène, et responsive, sur tous les types de client de messagerie. A moins que vous n'ayez quelques pistes fraiches ? N'hésitez pas à les partager dans les commentaires. 

Nous avons formatté notre mail au format HTML. Il ne nous reste qu'à l'envoyer.

L'envoi du mail

Tout simplement nous allons utiliser la fonction native de PHP utilisée par le Plugin par défaut PHPMail.

/**
 * {@inheritdoc}
 */
public function mail(array $message) {
  return parent::mail($message);
}

Si nous souhaitons utiliser un autre service d'envoi de mail, tel que Mandrill, ou un MTA local, nous pouvons le déclarer au sein de cette méthode. Vous trouverez par exemple une parfaite illustration dans ce billet Using and Extending the Drupal 8 Mail API: Part 2 pour utiliser Mandrill comme système d'envoi.

C'est parti !

Après ce long (trop long ?) billet, nous avons implémenté notre propre Plugin de Mail. Mais encore une fois, c'est une des méthodes (sans doute la plus fine car elle permet de contrôler toute la chaîne de production du mail) et non la seule, heureusement.

Le module SwiftMailer dispose d'une version déjà  fonctionnelle sur Drupal 8 (les modules MimeMail ou HTMLMail ne devraient pas tarder) et vous pourrez mieux saisir les options de configuration, que ce soit de ce module, ou du module Mail System permettant la configuration et le choix des méthodes de formatage et d'envoi des mails.

Il n'est d'ailleurs pas interdit de coupler à la fois la richesse fonctionnelle de SwiftMailer (qui fait un peu plus que le formatage, en gérant également l'attachement des pièces jointes, ou en injectant dans le corps du mail les images présentes dans le contenu) et une partie des différents mécanismes décrits dans ce billet. Drupal 8 offre toute une panoplie d'outils pour arriver à personnaliser aisément les courriers et contôler toute la chaîne d'envoi.

Vous souhaitez mieux maîtriser les mails qui sont émis pour votre site. Vous avez une problématique avec ces derniers ? N'hésitez pas à consulter un expert Drupal 8

 

Commentaires

Soumis par Noé (non vérifié) le 20/04/2016 à 09:39 - Permalien

Où se place le code dans la partie "Le déclenchement de l'envoi du mail" ? Dans le nom_module.module ? Dans le hook_entity_insert() ?

On peut l'utiliser dans n'importe quel hook (hook_entity_insert(), hook_entity_update(), etc.), ou fonction de submit, ou une Class, pour réagir à un événement. L'envoi du mail est réalisé forcément en fonction d'un événement (tout du moins dans 95% des cas), il faut donc placer cette partie de code dans le hook ou la fonction correspondante à cet événement.

Soumis par hanen (non vérifié) le 01/05/2016 à 01:19 - Permalien

La méthode "cleanBody($message)" dans la fonction "public function format(array $message)" est non définie dans cette classe. Où elle est déclarée !!!??

Bonjour. Tout d'abord merci pour votre question. Cette méthode est à déclarer bien sûr dans la Class. Pour des raisons de lisibilité, et ne pas surcharger inutilement le billet (déjà long), elle n'est pas détaillée dans l'article.

Ajouter un commentaire