Automatically integrate email via CSS styles with Drupal 8

colors

How do you put colors in your emails sent from your Drupal 8 site? Very often, integrating emails from a Drupal 8 site, or from any website, can be (very) time consuming, with strong constraints in terms of email rendering to have a correct rendering on all types of email or webmails. Not to mention here the responsive behavior of these emails, if only to respect the graphic charter of a web project (the Call to action buttons in the primary colors of the site, etc.).

One of the most efficient methods consists in injecting into the body of the mail, in HTML, the CSS styles to be used at the level of each HTML tag, what we call inline CSS.

For example, to integrate a button link, instead of using this type of tag

<a class="btn btn-primary" href="https://www.flocondetoile.fr">My link</a>

Instead, we should use this type of tag with the CSS styles included directly in the tag

<a href="https://www.flocondetoile.fr" style="padding: 6px 12px; color:#ff0000; border: 2px solid #ff0000; border-radius: 20px;">My link</a>

Notation that is much less flexible and modular in terms of rendering.

To achieve this, we can then either

  • Develop a specific template for each type of mail issued, which is rather tedious and above all defines in advance the position of the content elements that the mail will contain.
  • Automate the transformation of the CSS classes used, and thus preserve the original rendering on the site, in inline CSS styles.

It is of course this last option that I will detail here.

Put colors in your emails

To achieve our goal, we are not going to reinvent warm water and we are going to have a very efficient PHP library on this point, namely Emogrifier.

To add this library to our project, we use this command Compose

composer require pelago/emogrifier

In order to be able to emogrify all types of emitted mails, we are going to build a service that can be called as much as needed.

In the file my_module.services.yml, of the module MY_MODULE let's declare this service.

my_module.emogrify:
  class: Drupal\my_module\Emogrify
  arguments: [ '@config.factory', '@theme.manager', '@file_system', '@library.discovery', '@theme_handler', '@module_handler']

Then declare our Emogry Class in the src/ directory of the module. The concept of this service is based above all, to use correctly the Emogrifier library, on the recovery of the CSS files to be used to process an HTML string and transform all the CSS classes of the provided HTML into inline CSS style. You can of course use the CSS file of your main theme, but depending on its size, for performance reasons, it may be more relevant to provide a CSS file dedicated to theming mails (such as mail.css). And it also allows you to manage some CSS behaviors differently, on hovering for example, as well as to better target the rendering (some conflicts may appear depending on multiple CSS selectors with different behaviors that would be inliner in the same HTML tag).

This Class will therefore have a main method emogrify(), to be put in the Class interface, then various other methods, called help methods, to retrieve the right CSS files, depending on your project, methods that you will most likely have to adapt to your project.

For example, the getCssOverride() method retrieves the CSS file provided by a custom module, and if this file is missing, we perform a fallback to retrieve the CSS files from the 'mail' library of the default theme, with the getCssTheme() method. Note that we also support the Asset Injector module, by retrieving all the created CSS injector files that contain the string 'mail' in their identifier, with the method getCssInjector(). Of course this can be refined according to the needs of each project.

Here is the complete code as an example of our Emogriphy class.

namespace Drupal\my_module;

use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Theme\ThemeManagerInterface;
use Pelago\Emogrifier\CssInliner;

class Emogrify implements EmogrifyInterface {

  /**
   * Drupal\Core\Config\ConfigFactoryInterface definition.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected  $configFactory;

  /**
   * Drupal\Core\Theme\ThemeManagerInterface definition.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface
   */
  protected  $themeManager;

  /**
   * Drupal\Core\File\FileSystemInterface definition.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * Drupal\Core\Asset\LibraryDiscoveryInterface definition.
   *
   * @var \Drupal\Core\Asset\LibraryDiscoveryInterface
   */
  protected $libraryDiscovery;

  /**
   * Drupal\Core\Extension\ThemeHandlerInterface definition.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;

  /**
   * Drupal\Core\Extension\ModuleHandlerInterface definition.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Emogrify constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   */
  public function __construct(ConfigFactoryInterface $config_factory, ThemeManagerInterface $theme_manager, FileSystemInterface $file_system, LibraryDiscoveryInterface $library_discovery, ThemeHandlerInterface $theme_handler, ModuleHandlerInterface $module_handler) {
    $this--->configFactory = $config_factory;
    $this->themeManager = $theme_manager;
    $this->fileSystem = $file_system;
    $this->libraryDiscovery = $library_discovery;
    $this->themeHandler = $theme_handler;
    $this->moduleHandler = $module_handler;
  }

  /**
   * Gets the default theme.
   *
   * @return string
   *   The default theme.
   */
  protected function getDefaultTheme() {
    $default_theme = $this->configFactory->get('system.theme')->get('default');
    return $default_theme;
  }

  /**
   * Get all the libraries given a theme.
   *
   * @param $theme
   * @return array
   */
  protected function getThemeLibraries($theme) {
    $libraries = [];
    $themes = $this->themeHandler->rebuildThemeData();
    $root = \Drupal::root();

    if (isset($themes[$theme])) {
      /** @var \Drupal\Core\Extension\Extension $extension */
      $extension = $themes[$theme];
      $library_file = $extension->getPath() . '/' . $theme . '.libraries.yml';
      if (is_file($root . '/' . $library_file)) {
        $libraries = $this->libraryDiscovery->getLibrariesByExtension($theme);
      }
    }
    return $libraries;
  }

  /**
   * Gets the libraries to use for the CssInliner.
   *
   * @return array
   */
  protected function getCssLibraries() {
    return [
      'mail',
    ];
  }

  /**
   * Gets the css to apply on mail.
   *
   * @return string
   */
  protected function getCss($theme) {
    $css = $this->getCssOverride();
    if (empty($css)) {
      $css = $this->getCssTheme($theme);
    }
    $css_injector = $this->getCssInjector();
    if (!empty($css_injector)) {
      $css = $css . PHP_EOL . $css_injector;
    }
    return $css;
  }

  /**
   * Gets the css from default theme.
   *
   * @return string
   */
  protected function getCssTheme($theme) {
    $css = '';
    $libraries = $this->getThemeLibraries($theme);
    $css_libraries = $this->getCssLibraries();
    foreach ($css_libraries as $css_library) {
      if (isset($libraries[$css_library]['css'])) {
        $theme_css_files = $libraries[$css_library]['css'];
        foreach ($theme_css_files as $key => $theme_css_file) {
          if (empty($theme_css_file['data'])) {
            continue;
          }
          $data = $theme_css_file['data'];
          $data_file = $this->fileSystem->realpath($data);
          $css = $css . PHP_EOL . file_get_contents($data_file);
        }
      }
    }
    return $css;
  }

  /**
   * Gets the css override from usine_theme module.
   *
   * @return string
   */
  protected function getCssOverride() {
    $css = '';
    if ($this->moduleHandler->moduleExists('usine_theme')) {
      /** @var \Drupal\usine_theme\ManagerAssetInterface $manager */
      $manager = \Drupal::service('usine_theme.manager');
      if ($manager->hasAssetOverride('color')) {
        $data = $manager->getAsset('mail');
        if ($data) {
          $data = ltrim($data, '/');
          $data_file = $this->fileSystem->realpath($data);
          $css = file_get_contents($data_file);
        }
      }
    }
    return $css;
  }

  /**
   * Gets the css for asset_injector module.
   *
   * @return string
   */
  protected function getCssInjector() {
    $css = '';
    if ($this->moduleHandler->moduleExists('asset_injector')) {
      foreach (asset_injector_get_assets(TRUE, ['asset_injector_css']) as $asset) {
        if (strpos($asset->id(), 'mail') !== FALSE) {
          $css_content = $asset->getCode();
          $css = $css . PHP_EOL . $css_content;
        }
      }
    }
    return $css;
  }

  /**
   * {@inheritdoc}
   */
  public function emogrify($html, $theme = 'default') {
    if ($theme === 'default') {
      $theme = $this->getDefaultTheme();
    }
    if ($html instanceof Markup) {
      $html = (string) $html;
    }
    if (!is_string($html)) {
      return $html;
    }
    $css = $this->getCss($theme);
    if (empty($css)) {
      return $html;
    }
    Try {
      $html_inline_css = CssInliner::fromHtml($html)->inlineCss($css)->renderBodyContent();
    }
    catch (\Exception $e) {
      return $html;
    }
    $html = Markup::create($html_inline_css);
    return $html;
  }
}

All we have to do is use our service to automatically replace all CSS classes contained in the body of the mail with their corresponding inline CSS style that will have been found in the CSS files provided.

An implementation of hook_mail_alter() will be sufficient.

/**
 * Implements hook_mail_alter().
 */
function my_module_mail_alter(&$message) {
  // Tell to SwiftMailer to use our template for email body.
  $message['params']['theme'] = 'mail_base';
  /** @var \Drupal\my_module\EmogrifyInterface $emogrify */
    $emogrify = \Drupal::service('my_module.emogrify');
    if (!empty($message['body'])) {
      foreach ($message['body'] as $key => $body) {
        $body = $emogrify->emogrify($body);
        $message['body'][$key] = new FormattableMarkup($body, []);
    }
  }
}

We can then simply enrich the content of the emails sent for our Drupal 8 project, or even customize the rendering of the emails as needed, by adding CSS rules in our library or via CSS injector, depending on the context. And this can mean, at least for a good part, the end of the Drupal 8 developer's anxiety when it comes to integrating a new type of mail for his project.

 

Ajouter un commentaire