Отправка электронной почты в формате HTML

Введение


Почти в каждом проекте приходится думать об отправке писем по электронной почте. Основными требованиями при этом являются, помимо надежности доставки, привлекательность и удобство электронных писем.


Основные нюансы при формировании таких писем:


  • Все стили должны встраиваться (inline) в виде атрибута style для конкретного HTML-элемента.
  • Все изображения должны встраиваться, либо как отдельные вложения в в письме, либо в виде base64-кодированных данных (второе банально удобнее).
  • Письмо должно поддерживать DKIM (настройка мэйлера), а домен отправителя — содержать SPF-запись.

Ранее я использовал для формирования HTML-писем проект Premailer, созданный на Ruby. Пришлось даже заняться поддержкой проекта (сейчас времени на это нет, мэйнтейнеры приветствуются).


Сейчас же хотелось избежать внедрения Ruby, в то время, как Node проник везде.


Juice


К счастью, современная экосистема Node предоставляет богатые возможности по формированию электронных писем. Мы выбрали цепочку по формированию электронной почты в виде pug-шаблонов, преобразованию оных с помощью juice и подстановки конкретных данных на бэкэнде (у нас это Perl).


Предполагается, что Вы используете node 6+, babel (es2015, es2016, es2017, stage-0 presets).


Установка


npm install gulp-cli -g
npm install gulp --save-dev
npm install del --save-dev
npm install gulp-rename --save-dev
npm install gulp-pug --save-dev
npm install premailer-gulp-juice --save-dev
npm install gulp-postcss --save-dev
npm install autoprefixer --save-dev
npm install gulp-less --save-dev

gulpfile.babel.js:


'use strict';

import gulp from 'gulp';
import mail from './builder/tasks/mail';
gulp.task('mail', mail);

builder/tasks/mail.js:


'use strict';

import gulp from 'gulp';
import stylesheets from './mail/stylesheets';
import templates from './mail/templates';
import clean from './mail/clean';

const mail = gulp.series(clean, stylesheets, templates);

export default mail;

builder/tasks/mail/stylesheets.js


'use strict';

import gulp from 'gulp';
import config from 'config';
import rename from 'gulp-rename';
import postcss from 'gulp-postcss';
import autoprefixer from 'autoprefixer';
import less from 'gulp-less';

const stylesheetsPath = config.get('srcPath') + '/mail/stylesheets';

const stylesheetsGlob = stylesheetsPath + '/**/*.less';

const mailStylesheets = () => {
  return gulp.src(stylesheetsGlob)
    .pipe(less())
    .pipe(postcss([
      autoprefixer({browsers: ['last 2 versions']}),
    ]))
    .pipe(gulp.dest(stylesheetsPath));
};

export default mailStylesheets;

builder/tasks/mail/templates.js:


'use strict';

import gulp from 'gulp';
import config from 'config';
import pug from 'gulp-pug';
import rename from 'gulp-rename';
import juice from 'premailer-gulp-juice';

const templatesPath = config.get('srcPath') + '/mail';
const mailPath = config.get('mailPath');

const templatesGlob = templatesPath + '/**/*.pug';

const mailTemplates = () => {
  return gulp.src(templatesGlob)
    .pipe(rename(path => {
      path.extname = '.html';
    }))
    .pipe(pug({
      client: false
    }))
    .pipe(juice({
      webResources: {
        relativeTo: templatesPath,
        images: 100,
        strict: true
      }
    }))
    .pipe(gulp.dest(mailPath));
};

export default mailTemplates;

builder/tasks/mail/clean.js:


'use strict';

import del from 'del';
import gutil from 'gulp-util';

const clean = done => {
  return del([
    'mail/*.html',
    'src/mail/stylesheets/*.css'
  ]).then(() => {
    gutil.log(gutil.colors.green('Delete src/mail/stylesheets/*.css and mail/*.html'));
    done();
  });
};

export default clean;

Типичный шаблон выглядит так (generic.pug):


include base.pug

+base
    tr(height='74')
        td.b-mail__table-row--heading(align='left', valign='top') Привет,
    tr
        td(align='left', valign='top')
            | <%== $html %>

Где base.pug:


mixin base(icon, alreadyEncoded)
    doctype html
    head
        meta(charset="utf8")
        link(rel="stylesheet", href="/stylesheets/mail.css")
    body
        table(width='100%', border='0', cellspacing='0', cellpadding='0')
            tbody
                tr
                    td.b-mail(align='center', valign='top', bgcolor='#ffffff')
                        br
                        br
                        table(width='750', border='0', cellspacing='0', cellpadding='0')
                            tbody.b-mail__table
                                tr.b-mail__table-row(height='89')
                                tr.b-mail__table-row
                                    td(align='left', valign='top', width='70')
                                        img(src='/images/logo.jpg')
                                    td(align='left', valign='top')
                                        table(width='480', border='0', cellspacing='0', cellpadding='0')
                                            tbody
                                                if block
                                                    block
                                    td(align='right', valign='top')
                                        if alreadyEncoded
                                            img.fixed(src!=icon, data-inline-ignore)
                                        else if icon
                                            img.fixed(src!=icon)
                        br
                        br
                tr
                    td(align='center', valign='top')

Собственно, болванка готова, шаблоны компилируются. Формирование модуля config тривиально и необязательно.


Готовая болванка репозитория здесь: https://github.com/premailer/gulp-juice-demo


gulp mail

ViewAction


Многие почтовые клиенты, такие, как GMail/Inbox, поддерживают специальные действия в режиме просмотра сообщений. Внедрить их проще простого, добавив в содержимое сообщения следующие тэги:


div(itemscope, itemtype="http://schema.org/EmailMessage")
  div(itemprop="action", itemscope, itemtype="http://schema.org/ViewAction")
    link(itemprop="url", href="https://github.com/imlucas/gulp-juice/pull/9")
    meta(itemprop="name", content="View Pull Request")
  meta(itemprop="description", content="View this Pull Request on GitHub")

Ну и немного интеграции с (выберите свой язык, тут нужен был Perl)


sub prepare_mail_params {
    my %params = %{ shift() };

    my @keys = keys %params;
    # Camelize params
    for my $param ( @keys ) {
        my $new_param = $param;
        $new_param =~ s/^(\w)/\U$1\E/;
        next  if $new_param eq $param;
        $params{$new_param} = delete $params{$param};
    }

    %params = (
        Type     => 'multipart/mixed; charset=UTF-8',
        From     => 'support@ourcompany.co.uk',
        Subject  => '',
        %params,
    );

   # Mime params
    for my $param ( keys %params ) {
        $params{$param} = encode( 'MIME-Header', $params{$param} );
    }

    return \%params;
}

sub _template_processor {
    state $instance = Mojo::Template->new(
        vars        => 1,
        auto_escape => 1,
    );
    return $instance;
}

sub send_mail {
    my %params = %{ shift() };

    my $html =  (delete $params{message}) // '';

    my $template =  delete $params{template};
    my $stash = (delete $params{stash}) // {};
    unless ( $template ) {
        $template = 'generic';
        $stash->{html} = $html;
    }

    $html = _template_processor()->render_file(
        Config->directories->{mail}. "/$template.html",
        $stash,
    );

    $html = encode_utf8( $html );

    my $msg = MIME::Lite->new(
        %{ prepare_mail_params( \%params ) }
    );

    $msg->attach(
        Type => 'HTML',
        Data => $html,
    );

    if ( $mail_settings->{method} eq 'sendmail' ) {
        return $msg->send();
    }

    if ( $mail_settings->{method} eq 'smtp' ) {
        return $msg->send('smtp', $mail_settings->{host}, Timeout => $mail_settings->{timeout});
    }

    croak "Unknown Config mail.method: ". $mail_settings->{method};
}

Полезные ссылки


  • Исходный код проекта по данной статье.
  • Подборка: 40+ полезных инструментов, ресурсов и исследований о работе с email

P.S.: Спасибо pstn за доработки шаблонов писем.

Комментарии (1)

  • 17 декабря 2016 в 11:38

    0

    Посмотрите на http://foundation.zurb.com/emails.html
    У них свои html теги, позволяют избавиться от миллиона вложенных таблиц в шаблонах. У меня в двух проектах с успехом работают, письма отображаются идеально на всех веб-клиентах и Apple системах. Есть небольшие проблемы с андроидом, огромные проблемы с аутлуком, как и всегда.

© Habrahabr.ru