In the previous article we looked at how we can send emails programatically in Drupal 8. We also saw how other modules can alter these outgoing mails. Today, we are going to look at how we can use the Mail API to extend this default behaviour. The purpose is to use an external service as a means for email delivery.
For this, we will use Mandrill, although the focus of the article will not be its API or how to work with it, but rather the Drupal side of things. And remember, the working module can be found in this Git repository.
As we've seen in the previous article, sending an email in Drupal 8 happens by requesting the mail manager, passing some parameters to its mail()
method, and setting up a template inside a hook_mail()
implementation. What the mail manager does internally is load up the appropriate mail plugin, construct the email, and then delegate to the mail()
method of whatever plugin was loaded.
But who does it actually delegate to?
Plugin Selection
An important thing to understand before writing our own plugin is the selection process of the mail manager for loading plugins. In other words, how do we know which plugin it will load, and how can we make it load our own?
The system.mail.interface
configuration array holds all the answers. It contains the ids of the available plugins, keyed by the context they are used in. By default, all we have inside this configuration is default => phpmail
. This means that the plugin with the id phpmail
(the PHPMail class) is used as fallback for all contexts that are not otherwise specified, i.e. the default.
If we want to write our own plugin, we need to add another element into that array with the plugin id as its value. The key for this value can be one of two things: the machine name of our module (to load the plugin whenever our module sends emails) or a combination of module name and email template key (to load the plugin whenever our module sends an email using that specific key).
An example of the latter construct is d8mail_node_insert
, where d8mail
is our module name we started building in the previous article, and node_insert
is the email template key we defined.
So now that we know how the mail plugin selection happens, we need to make sure this config array contains the necessary information so that emails sent with our d8mail
module use the new plugin we will build. We can do this inside a hook_install() implementation that gets triggered only once when the module gets installed:
d8mail.install:
1 /** 2 * Implements hook_install(). 3 */ 4 function d8mail_install() { 5 $config = \Drupal::configFactory()->getEditable('system.mail'); 6 $mail_plugins = $config->get('interface'); 7 if (in_array('d8mail', array_keys($mail_plugins))) { 8 return; 9 } 10 11 $mail_plugins['d8mail'] = 'mandrill_mail'; 12 $config->set('interface', $mail_plugins)->save(); 13 }Not super complicated what happens above. We load the editable config object representing the system.mail
configuration, and add a new element to the interface
array: d8mail => mandrill_mail
. We will soon create a mail plugin with the id of mandrill_mail
which will be used for all emails sent by the d8mail
module. And that's it.
But before we move on, we need to make sure this change is reverted when the module is uninstalled. For this, we can use the counterpart hook_uninstall() that gets called when a module gets uninstalled (there is no more module disabling in Drupal 8).
Inside the same file:
1 /** 2 * Implements hook_uninstall(). 3 */ 4 function d8mail_uninstall() { 5 $config = \Drupal::configFactory()->getEditable('system.mail'); 6 $mail_plugins = $config->get('interface'); 7 if ( ! in_array('d8mail', array_keys($mail_plugins))) { 8 return; 9 } 10 11 unset($mail_plugins['d8mail']); 12 $config->set('interface', $mail_plugins)->save(); 13 }With the hook_uninstall()
implementation we do the opposite of before: we remove our plugin id if it is set.
The install/uninstall scenario is just one way to go. You can also create an administration form that allows users to select the plugin they want and under which context. But you still need to make sure that when you disable the module defining a particular plugin, the configuration will no longer keep a reference to that plugin. Otherwise the mail manager may try to use a non-existent class and throw all kinds of errors.
Mandrill
As I mentioned before, we will work with the Mandrill API in order to illustrate our task. So let's load up Mandrill's PHP Library and make it available in our environment. There are three steps we need to do for this.
First, we need to actually get the library inside Drupal. At the time of writing, this basically means adding the "mandrill/mandrill": "1.0.*"
dependency to the root composer.json
file and running composer install
. Keep in mind, though, that this will also clear the Drupal installation from inside the core/
folder and download the latest stable release instead. Then, you'll need to edit the root index.php
file and change the path to the autoloader as per these instructions. Hopefully this last action won't be necessary soon, and I encourage you to follow the discussions around the future of Composer in Drupal 8 for managing external libraries.
Second, we need to get an API key from Mandrill. Luckily, this we can easily generate from their administration pages. Once we have that, we can store it inside a new file created on our server, at either location:
1 ~/.mandrill.key 2 /etc/mandrill.keyWe can also pass the key as a constructor parameter to the main Mandrill
class, but this way we won't have to hardcode it in our code.
Thirdly, we need to create a service so that we can use dependency injection for passing the Mandrill
class into our plugin:
d8mail.services.yml:
1 services: 2 d8mail.mandrill: 3 class: MandrillDepending on how you have loaded the Mandrill
class into your application, you'll need to change the value after class
. By using the composer.json
approach, this will suffice.
The Mail Plugin
It's finally time to create our plugin. In Drupal 8, plugin classes go inside the src/Plugin
folder of our module. Depending on their type, however, they are placed further down within other directories (in our case Mail
). Let's write our class that will depend on the Mandrill API library to send emails:
src/Plugin/Mail/MandrillMail.php:
1 <?php 2 3 namespace Drupal\d8mail\Plugin\Mail; 4 5 use Drupal\Core\Mail\MailFormatHelper; 6 use Drupal\Core\Mail\MailInterface; 7 use Drupal\Core\Plugin\ContainerFactoryPluginInterface; 8 use Symfony\Component\DependencyInjection\ContainerInterface; 9 use Mandrill; 10 use Mandrill_Error; 11 12 /** 13 * Defines the Mandrill mail backend. 14 * 15 * @Mail( 16 * id = "mandrill_mail", 17 * label = @Translation("Mandrill mailer"), 18 * description = @Translation("Sends an email using Mandrill.") 19 * ) 20 */ 21 class MandrillMail implements MailInterface, ContainerFactoryPluginInterface { 22 23 /** 24 * @var Mandrill 25 */ 26 private $mandrill; 27 28 /** 29 * @param Mandrill $mandrill 30 */ 31 public function __construct(Mandrill $mandrill) { 32 $this->mandrill = $mandrill; 33 } 34 35 /** 36 * {@inheritdoc} 37 */ 38 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 39 return new static( 40 $container->get('d8mail.mandrill') 41 ); 42 } 43 44 /** 45 * {@inheritdoc} 46 */ 47 public function format(array $message) { 48 // Join the body array into one string. 49 $message['body'] = implode("\n\n", $message['body']); 50 // Convert any HTML to plain-text. 51 $message['body'] = MailFormatHelper::htmlToText($message['body']); 52 // Wrap the mail body for sending. 53 $message['body'] = MailFormatHelper::wrapMail($message['body']); 54 55 return $message; 56 } 57 58 /** 59 * {@inheritdoc} 60 */ 61 public function mail(array $message) { 62 63 try { 64 $vars = [ 65 'html' => $message['body'], 66 'subject' => $message['subject'], 67 'from_email' => $message['from'], 68 'to' => array( 69 array('email' => $message['to']) 70 ), 71 ]; 72 73 $result = $this->mandrill->messages->send($vars); 74 if ($result[0]['status'] !== 'sent') { 75 return false; 76 } 77 78 return $result; 79 } 80 catch (Mandrill_Error $e) { 81 return false; 82 } 83 } 84 }There are a couple of things to note before getting into what the class does.
First, the annotations above the class. This is just the most common plugin discovery mechanism for Drupal 8. The id
key matches the value we added to the system.mail.interface
configuration array earlier, while the rest are basic plugin definition elements.
Second, the implementation of the ContainerFactoryPluginInterface
interface by which we define the create()
method. The latter is part of the dependency injection process by which we can load up the Mandrill service we defined in the services.yml
file earlier. This makes testing much easier and it's considered best practice.
As I mentioned, the mail plugins need to implement the MailInterface
interface which enforces the existence of the format()
and mail()
methods. In our case, the first does exactly the same thing as the PHPMail
plugin: a bit of processing of the message body. So you can add your own logic here if you want. The latter method, on the other hand, is responsible for sending the mail out, in our case, using the Mandrill API itself.
As the Mandrill documentation instructs, we construct an email message inside the $vars
array using values passed from the mail manager through the $message
parameter. These will be already filtered through hook_mail()
, hook_mail_alter()
and the plugin's own format()
method. All that's left is to actually send the email. I won't go into the details of using the Mandrill API as you can consult the documentation for all the options you can use.
After sending the email and getting back from Mandrill a sent
status, we return the entire response array, which contains some more information. This array then gets added by the mail manager to its own return array keyed as result
. If Mandrill has a problem, rejects the email or throws an exception, we return false
. This will make the mail manager handle this situation by logging the incident and printing a status message.
And that is pretty much it. We can clear the cache and try creating another article node. This time, the notification email should be sent by Mandrill instead of PHP's mail()
. With this in place, though, the hook_mail_alter()
implementation has become superfluous as there are no headers we are actually sending through to Mandrill (and the text is HTML already). And for that matter quite a lot of the work of the mail manager is not used, as we are not passing that on to Mandrill. But this is just meant to illustrate the process of how you can go about setting this up. The details of the implementation remain up to you and your needs.
Conclusion
And there we have it. We have implemented our own mail plugin to be used by the d8module
we started in the previous article. And due to the extensible nature of Drupal 8, it didn't even take too much effort.
What's left for you to do is to perfect the mail sending logic and adapt it to your circumstances. This can mean further integration between Mandrill and your Drupal instance, constructing nice templates or what have you. Additionally, an important remaining task is writing automated tests for this functionality. And Drupal 8 offers quite the toolkit for that as well.