How to send custom html mailers using symfony mailer module in Drupal 9 and Drupal 10

Here we are discussing how to create our own html mail templates and sending mails with your custom values embedded in those mail templates,

Install Symfony mailer module and configure as shown in this article.

You can see Types and subtype in Mailer policy of installed module configuration.

Here in this article we are going to create our own Type and Subtype for mail policy and send custom html mails with with CSS and variable values in html body of the mailer. Also here we will discuss how to theme different  subtype of mail policy with its own templates. 

As per drupal.org documentation you can see theming sub type can be achieved by naming templates as below 

  • email–TYPE–SUBTYPE–ENTITY-ID
  • email–TYPE–SUBTYPE
  • email–TYPE

But as per my check, this is not working as expected, Here We will discuss how to achieve this.

In our sample code we are going to send  two custom html mails, one for daily email and another for weekly mail, so mail will be send to recipients based selection in below custom form.

So here we are following below steps  in our custom module to achieve this.

Assuming you have installed and configure symfony mailer module.

  1. Create policy type and sub type
  2. Create templates for Daily and weekly mails
  3. Preprocess hooks for email templates and theme function
  4. Create Email Builder Plugin
  5. Send mail for specific subtype using a custom form

We are going to implement this emails sending functionality in a custom module called – dn_mail. Download sample module here.

1.Create policy type and sub type

While installing module we have to create below  policy Type and policy sub type programmatically in path Configuration> Mailer  (admin/config/system/mailer).

Create below file in your custom module path /config /install

/dn_mail/config/install/symfony_mailer.mailer_policy.dn_mail.daily.yml

Content of this daily policy will be as below.

langcode: en
status: true
dependencies: {}
id: dn_mail.daily
configuration:
  email_body:
    content:
      value: |-
        <h4>You have Daily  mail</h4>
        <p>This is a test email from <a href="www.digitalnadeem.com">DigitalNadeem</a>.</p>
        <p>Have a great <span class="day">{{ day }}</span>!</p>
        <h4 class="text-small">
           <a href="https://www.drupal.org/project/symfony_mailer">Drupal Symfony Mailer</a>
        </h4>
      format: email_html
  email_subject:
    value: 'Test email from '

/dn_mail/config/install/symfony_mailer.mailer_policy.dn_mail.weekly.yml

Content of this weekly policy will be as below.

langcode: en
status: true
dependencies: {}
id: dn_mail.weekly
configuration:
  email_body:
    content:
      value: |-
        <h4>You have weekly mail</h4>
        <p>This is a test email from <a href="www.digitalnadeem.com">DigitalNadeem</a>.</p>
        <p>Have a great <span class="day">{{ day }}</span>!</p>
        <h4 class="text-small">
           <a href="https://www.drupal.org/project/symfony_mailer">Drupal Symfony Mailer</a>
        </h4>
      format: email_html
  email_subject:
    value: 'Test email from [site:name]'

Here note this, id you have to provide first your module name then policy sub type.

id:  your_module.subtype

After installation of this module, you could see these policies in  in path Configuration> Mailer  (admin/config/system/mailer).

You can edit policies in this configuration page after installation of custom module.

2.Create templates for Daily and weekly mails

Inside your module  templates folder create two html templates for daily and weekly mail.

As below 

/dn_mail/templates/email-daily.html.twig

{#
/**
 * @file
 * Default theme implementation for Symfony Email wrapper.
 *
 * Variables:
 * - body: Email body content.
 * - is_html: True if generating HTML output, false for plain text.
 * - subject: Email subject.
 * - type: Email type.
 * - sub_type: Email sub-type.
 * - attributes: HTML attributes for the top-level email element.
 *
 * @see template_preprocess_email_wrap()
 *
 * @ingroup themeable
 */
#}
{%
  set classes = [
    'email-type-' ~ type|clean_class,
    'email-sub-type-' ~ sub_type|clean_class,
  ]
%}

{% if is_html %}
<html>
<body>
<div{{ attributes.addClass(classes) }}>

  <table width="100%" cellpadding="0" cellspacing="0">
  <h1> Inside Daily out template--Welcome--</h1>
    <tr>
      <td>
        <div style="padding: 0px 0px 0px 0px;" class="clearfix">
          {{ body }}
        </div>
      </td>
    </tr>
  </table>
</div>
</body>
</html>
{% else %}
{{ body }}
{% endif %}

/dn_mail/templates/email-weekly.html.twig

{#
/**
 * @file
 * Default theme implementation for Symfony Email wrapper.
 *
 * Variables:
 * - body: Email body content.
 * - is_html: True if generating HTML output, false for plain text.
 * - subject: Email subject.
 * - type: Email type.
 * - sub_type: Email sub-type.
 * - attributes: HTML attributes for the top-level email element.
 *
 * @see template_preprocess_email_wrap()
 *
 * @ingroup themeable
 */
#}
{%
  set classes = [
    'email-type-' ~ type|clean_class,
    'email-sub-type-' ~ sub_type|clean_class,
  ]
%}

{% if is_html %}
<html>
<body>
<div{{ attributes.addClass(classes) }}>

  <table width="100%" cellpadding="0" cellspacing="0">
  <h1 class="header"> Inside weekly out template----</h1>
    <tr>
      <td>
        <div style="padding: 0px 0px 0px 0px;" class="clearfix">
          {{ body }}
        </div>
      </td>
    </tr>
  </table>
</div>
</body>
</html>
{% else %}
{{ body }}
{% endif %}

In both cases you can see {{body}}  variable we are printing,  this content  will be from our policy sub type.

Preprocess hooks for email templates and theme function 

Path – /dn_mail/dn_mail.module

create below hook_theme() and template preprocess hook for weekly and daily templates.

/**
* Implements hook_theme().
*/
function dn_mail_theme($existing, $type, $theme, $path) {
 
  return [
   'email_dnmail' => [
     'variables' => [
       'email' => NULL,
       'body' => '',
       'is_html' => TRUE,
     ],
   ],
   
   'email_daily' => [
     'variables' => [
       'email' => NULL,
       'body' => '',
       'is_html' => TRUE,
     ],
   ],


   'email_weekly' => [
     'variables' => [
       'email' => NULL,
       'body' => '',
       'is_html' => TRUE,
     ],
   ],


 ];
}


Template preprocess for daily and weekly templates.

/**
* Prepares variables for email wrap template.
*
* Default template: email-dnmail-weekly.html.twig.
*
* @param array $variables
*   An associative array containing:
*   - email: Email object.
*   - body: Body string.
*   - is_html: True if generating HTML output, false for plain text.
*/
function template_preprocess_email_dnmail_weekly(array &$variables) {
 $email = $variables['email'];
 $variables['subject'] = $email->getSubject();
 $variables['type'] = $email->getType();
 $variables['sub_type'] = $email->getSubType();
 $variables['attributes'] = new Attribute();
}

/**
 * Prepares variables for email daily template.
 *
 * Default template: email-dnmail-weekly.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - email: Email object.
 *   - body: Body string.
 *   - is_html: True if generating HTML output, false for plain text.
 */
function template_preprocess_email_daily(array &$variables) {
  $email = $variables['email'];
  $variables['subject'] = $email->getSubject();
  $variables['type'] = $email->getType();
  $variables['sub_type'] = $email->getSubType();
  $variables['attributes'] = new Attribute();
}

Create Email Builder Plugin

This is the major step in creating custom html mails, we have to create a plugin class which extends EmailBuilderBase class.

So in this class we have to provide below Email builder annotations.

<?php

namespace Drupal\dn_mail\Plugin\EmailBuilder;

use Drupal\symfony_mailer\EmailFactoryInterface;
use Drupal\symfony_mailer\EmailInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\symfony_mailer\Processor\EmailBuilderBase;
use Drupal\symfony_mailer\Processor\TokenProcessorTrait;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\RendererInterface;
use Drupal\symfony_mailer\MailerHelperTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Defines the Email Builder plug-in for test mails.
*
* @EmailBuilder(
*   id = "dn_mail",
*   label = @Translation("DN Mails"),
*   sub_types = { "weekly" = @Translation("Weekly email"), "daily" = @Translation("Daily email") },
*   common_adjusters = {"email_subject", "email_body"},
* )
*/
class CampaignEmailBuilder extends EmailBuilderBase   {

Here id is the custom module name , also provides labels and sub types, so in policy page, you can see type as DN Mails and two subtypes daily and weekly created after installation of this module.

Also in this plugin, we are overriding render function with our custom templates  as below.

/**
  * Renders a body string using the wrapper template.
  *
  * @param \Drupal\symfony_mailer\EmailInterface $email
  *   The email being processed.
  * @param string $body
  *   The body string to wrap.
  * @param bool $is_html
  *   True if generating HTML output, false for plain text.
  *
  * @return \Drupal\Component\Render\MarkupInterface
  *   The wrapped body.
  */
 protected function render(EmailInterface $email, string $body, bool $is_html) {
    
     switch ($email->getSubType()) {
       case 'weekly':
         $render = [
           '#theme' => 'email_weekly',
           '#email' => $email,
           '#body' => Markup::create($body),
           '#is_html' => $is_html,
         ];
         break;
       case 'daily':
         $render = [
           '#theme' => 'email_daily',
           '#email' => $email,
           '#body' => Markup::create($body),
           '#is_html' => $is_html,
         ];
         break;
       default:
       $render = [
         '#theme' => 'email_dnmail',
         '#email' => $email,
         '#body' => Markup::create($body),
         '#is_html' => $is_html,
       ];
        
     }
    
   
   


   return $this->renderer->renderPlain($render);
 }



Here based on what subtype the corresponding template will be returned before building the mailer.

Then create a post render function as below.

/**
 * {@inheritdoc}
 */
public function postRender(EmailInterface $email) {
  $orig_html = $email->getHtmlBody();
  $plain = $html = NULL;
  // echo "---post-render------";echo $orig_html;exit;
  if ($orig_html && !$this->configuration['plain']) {
    $html = $this->render($email, $orig_html, TRUE);
  }
  $email->setHtmlBody($html);




  if ($orig_plain = $email->getTextBody()) {
    // To wrap the plain text we need to convert to HTML to render the
    // template then convert back again. We avoid check_markup() as it would
    // convert URLs to links.
    // @todo Inefficient? Could set second parameter to `{{ body }}` then
    // search and replace with the actual body after.
    $plain = $this->render($email, _filter_autop(Html::escape($orig_plain)), FALSE);
  }
  elseif ($orig_html) {
    // Wrap plain text.
    $plain = $this->render($email, $orig_html, FALSE);
  }




  if ($plain) {
    $email->setTextBody($this->helper()->htmlToText($plain));
  }
}


Finally create build function where we can pass variables or add css library that we can apply it in policy or email templates, in below build function we are setting value for day   variable and adding weekly library which will import css file saved in path /dn_mail/css/weekly.email.css

public function build(EmailInterface $email) {
   
   // - Add a custom CSS library. The library is defined in
   //   \Drupal\symfony_mailer\symfony_mailer.libraries.yml. The CSS is
   //   defined in \Drupal\symfony_mailer\css\test.email.css.
   // - Set an parameter programmatically.
   //   The variable is used by the mailer policy which specifies the
   //   email title and body as defined in
   //   \Drupal\symfony_mailer\config\install\symfony_mailer.mailer_policy.symfony_mailer.test.yml.
   $email->addLibrary('dn_mail/weekly')
     ->setVariable('day', date("l"));
 }

Complete source code of  CampaignEmailBuilder plugin will be as show below

<?php

namespace Drupal\dn_mail\Plugin\EmailBuilder;

use Drupal\symfony_mailer\EmailFactoryInterface;
use Drupal\symfony_mailer\EmailInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\symfony_mailer\Processor\EmailBuilderBase;
use Drupal\symfony_mailer\Processor\TokenProcessorTrait;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\RendererInterface;
use Drupal\symfony_mailer\MailerHelperTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;

/**
 * Defines the Email Builder plug-in for test mails.
 *
 * @EmailBuilder(
 *   id = "dn_mail",
 *   label = @Translation("DN Mails"),
 *   sub_types = { "weekly" = @Translation("Weekly email"), "daily" = @Translation("Daily email") },
 *   common_adjusters = {"email_subject", "email_body"},
 * )
 */
class CampaignEmailBuilder extends EmailBuilderBase implements ContainerFactoryPluginInterface{

  use TokenProcessorTrait;
  use MailerHelperTrait;

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

/**
  * Constructor.
  *
  * @param array $configuration
  *   A configuration array containing information about the plugin instance.
  * @param string $plugin_id
  *   The plugin ID for the plugin instance.
  * @param mixed $plugin_definition
  *   The plugin implementation definition.
  * @param \Drupal\Core\Render\RendererInterface $renderer
  *   The renderer.
  * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  *   The module handler to invoke the alter hook with.
  */
public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
   parent::__construct($configuration, $plugin_id, $plugin_definition);
   //parent::__construct($configuration);
   $this->renderer = $renderer;
   $this->moduleHandler = $module_handler;
  
 }


 /**
  * {@inheritdoc}
  */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
 
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('renderer'),
      $container->get('module_handler'),
    );
  }
 
  /**
  * {@inheritdoc}
  */
 public function postRender(EmailInterface $email) {
   $orig_html = $email->getHtmlBody();
   $plain = $html = NULL;
   // echo "---post-render------";echo $orig_html;exit;
   if ($orig_html && !$this->configuration['plain']) {
     $html = $this->render($email, $orig_html, TRUE);
   }
   $email->setHtmlBody($html);


   if ($orig_plain = $email->getTextBody()) {
     // To wrap the plain text we need to convert to HTML to render the
     // template then convert back again. We avoid check_markup() as it would
     // convert URLs to links.
     // @todo Inefficient? Could set second parameter to `{{ body }}` then
     // search and replace with the actual body after.
     $plain = $this->render($email, _filter_autop(Html::escape($orig_plain)), FALSE);
   }
   elseif ($orig_html) {
     // Wrap plain text.
     $plain = $this->render($email, $orig_html, FALSE);
   }


   if ($plain) {
     $email->setTextBody($this->helper()->htmlToText($plain));
   }
 }


  /**
   * Renders a body string using the wrapper template.
   *
   * @param \Drupal\symfony_mailer\EmailInterface $email
   *   The email being processed.
   * @param string $body
   *   The body string to wrap.
   * @param bool $is_html
   *   True if generating HTML output, false for plain text.
   *
   * @return \Drupal\Component\Render\MarkupInterface
   *   The wrapped body.
   */
  protected function render(EmailInterface $email, string $body, bool $is_html) {
  
     //echo "sub type".$email->getSubType();exit;
      switch ($email->getSubType()) {
        case 'weekly':
          $render = [
            '#theme' => 'email_weekly',
            '#email' => $email,
            '#body' => Markup::create($body),
            '#is_html' => $is_html,
          ];
          break;
        case 'daily':
          $render = [
            '#theme' => 'email_daily',
            '#email' => $email,
            '#body' => Markup::create($body),
            '#is_html' => $is_html,
          ];
          break;
        default:
        $render = [
          '#theme' => 'email_dnmail',
          '#email' => $email,
          '#body' => Markup::create($body),
          '#is_html' => $is_html,
        ];
          
      }
      
     
     

    return $this->renderer->renderPlain($render);
  }

  

  /**
   * {@inheritdoc}
   */
  public function build(EmailInterface $email) {
     
    // - Add a custom CSS library. The library is defined in
    //   \Drupal\symfony_mailer\symfony_mailer.libraries.yml. The CSS is
    //   defined in \Drupal\symfony_mailer\css\test.email.css.
    // - Set an parameter programmatically.
    //   The variable is used by the mailer policy which specifies the
    //   email title and body as defined in
    //   \Drupal\symfony_mailer\config\install\symfony_mailer.mailer_policy.symfony_mailer.test.yml.
    $email->addLibrary('dn_mail/weekly')
      ->setVariable('day', date("l"));
  }


}


Css file mapped in below file.

/dn_mail/dn_mail.libraries.yml

weekly:
 css:
   theme:
     css/weekly.email.css: {}

Css file is in below.

/dn_mail/css/weekly.email.css

This file having below content.

.header{


   background-color:DodgerBlue;
}

Send mail for specific subtype using a custom form

In order to test the emails templates we are creating a custom form and sending daily and weekly mail based on selection  by user.

For this, create a custom form in path – /dn_mail/src/Form/SendContact.php

<?php


namespace Drupal\dn_mail\Form;


use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\symfony_mailer\EmailFactoryInterface;
use Drupal\symfony_mailer\MailerHelperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;


/**
* Symfony Mailer test email form.
*/
class SendContact extends FormBase {




 /**
  * {@inheritdoc}
  */
 public function getFormId() {
   return 'symfony_mailer_test_form';
 }


 /**
  * {@inheritdoc}
  */
 public function buildForm(array $form, FormStateInterface $form_state) {
  
  
   $form['#tree'] = TRUE;


   $form['recipient'] = [
     '#title' => $this->t('Recipient'),
     '#type' => 'textfield',
     '#default_value' => '',
     '#description' => $this->t('Recipient email address. Leave blank to send to yourself.'),
   ];


   $form['recipient_name'] = [
     '#title' => $this->t('Recipient Name'),
     '#type' => 'textfield',
     '#default_value' => '',
     '#description' => $this->t('Recipient Name.'),
   ];


   $form['chk-mail-type'] = [
     '#type' => 'checkboxes',
     '#title' => 'Email Type',
     '#options' => array('daily' => t('Daily'),'weekly' => t('Weekly'),),
     '#attributes' => array(
       'class' => ['']
      
       )
   ];


   $form['actions']['#type'] = 'actions';
   $form['actions']['submit'] = [
     '#type' => 'submit',
     '#value' => $this->t('Send'),
     '#button_type' => 'primary',
   ];


   return $form;
 }


 /**
  * {@inheritdoc}
  */
 public function submitForm(array &$form, FormStateInterface $form_state) {
 
 
   $emailFactory = \Drupal::service('email_factory');
   $to = $form_state->getValue('recipient') ?: $this->currentUser();


   $recipient_name = $form_state->getValue('recipient_name');
   //echo "rec==".$recipient_name;exit;
   $mail_types = $form_state->getValue('chk-mail-type');
   $mail_type_values = array_filter($mail_types);
   //print_r($mail_type_values);exit;
   foreach ($mail_type_values as $mail_type){
     if( $mail_type == "daily"){
     $email = $emailFactory->newTypedEmail('dn_mail', 'daily')
         ->setTo($to)
         ->setVariable('name',$recipient_name)
         ->send();
         $message = "Daily Mail attempt made";
         $this->messenger()->addMessage($message);
     }
     if( $mail_type == "weekly" ){
         $email = $emailFactory->newTypedEmail('dn_mail', 'weekly')
         ->setTo($to)
         ->setVariable('name',$recipient_name)
         ->send();
         $message = "Weekly  Mail attempt made";
         $this->messenger()->addMessage($message);
     }
   }
   //echo "name is--".$email->getVariable('name');exit;
   $message = is_object($to) ?
     $this->t('An attempt has been made to send an email to you.') :
     $this->t('An attempt has been made to send an email to @to.', ['@to' => $to]);
   $this->messenger()->addMessage($message);


   /*if ($error = $email->getError()) {
     // @todo
   }*/
 }


}

In submit function, you can see based on selection of email type, we are sending mails.

So for daily mail as below.

$email = $emailFactory->newTypedEmail('dn_mail', 'daily')
         ->setTo($to)
         ->setVariable('name',$recipient_name)
         ->send();
         $message = "Daily Mail attempt made";
         $this->messenger()->addMessage($message);

For weekly mail as below.

$email = $emailFactory->newTypedEmail('dn_mail', 'weekly')
         ->setTo($to)
         ->setVariable('name',$recipient_name)
         ->send();
         $message = "Weekly  Mail attempt made";
         $this->messenger()->addMessage($message);

Here we are specifying type and subtype in newTypedEmail() function  and setting To and variable name

Add below in your module routing.yml for accessing form.

dn_mail.contact:
 path: '/contact-form'
 defaults:
   _title: 'Send Mailer'
   _form: '\Drupal\dn_mail\Form\SendContact'
 requirements:
   _access: 'TRUE'

Access your page /contact-form

Download the sample module here

After submit you can see below mails received based on your E mail type selection.

Daily mail 

Weekly mail

Get Free E-book
Get a free Ebook on Drupal 8 -theme tutorial
I agree to have my personal information transfered to MailChimp ( more information )
if you like this article Buy me a Coffee this will be an ispiration for me to write articles like this.

You may also like...