Введение


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


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


  • Все стили должны встраиваться (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")

Подробнее можно прочесть в разделе Email Markup.


Ну и немного интеграции с (выберите свой язык, тут нужен был 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};
}

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



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

Поделиться с друзьями
-->

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


  1. A1MaZ
    17.12.2016 11:38

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


    1. akzhan
      17.12.2016 12:56

      Спасибо, интересное развитие Ink. Собственно, его CSS-часть вполне удобно использовать (Foundation for Emails 2) совместно.


      Остальное чуть позже гляну.


    1. webirus
      17.12.2016 13:33

      Поэтому у нас в компании все ещё верстают руками, без инструментов. Зато письма смотрятся одинаково хорошо, как на телефонах, так и на компах, даже в аутлуке письма выглядят точно также, как на макете. Сборщики не могут учесть всех особенностей.


      1. A1MaZ
        17.12.2016 14:27

        Если не секрет, покажите скрин какого-то письма?


        1. webirus
          17.12.2016 17:14

          Для всеобщей публикации не подходящее письмо выбрал, может найду что-то другое. Но в ЛС отправил.


    1. Radiocity
      17.12.2016 22:29

      А как дела обстоят с мобильными клиентами мейла и яши?)


      1. A1MaZ
        17.12.2016 23:38

        Мы тестируем через litmus, там их нет, поэтому не знаю. Напрягу тестировщиков в понедельник, не подумал об этом, спасибо. Я на айфоне пользуюсь клиентом email, в нем все ок. Подозреваю, что в айфонах должно быть все ок, а с андроида у нас 4% входов всего.


      1. A1MaZ
        19.12.2016 12:37

        Проверили на иос сторонние клиенты. Яху немного поехал, но не критично, все остальные в норме. Разве что некоторые не смогли понять, что нужно показать мобильную версию, а показали десктопную.

        Мейл не проверяли — у нас стартап для штатов и мейла ни у кого нет.

        Плюс еще проблема в том, что у нас письма нарисовал какой-то крутой американский дизайнер, который жил в своем отдельном дизайнерском мире и проблемы верстки писем его совсем не волновали. Нам пришлось подстраиваться под него.


  1. Icewild
    17.12.2016 12:58

    В чем преимущества перед https://mjml.io/?


    1. akzhan
      17.12.2016 13:02

      Интересный язык разметки — MJML.


      Надо казать, что это не замена, а удобный препроцессор, на замену тому же gulp-pug.


      Благо предоставляется пакет gulp-mjml.


    1. akzhan
      17.12.2016 13:23

      Вот как можно переделать


      // header.mjml.pug
      mj-section
        mj-column
          mj-text This is a header

      далее gulp-pug, gulp-mjml, и напоследок juice.


      На самом деле весь juice и не нужен, достаточно сделать gulp-обёртку вокруг web-resource-inliner.


  1. webirus
    17.12.2016 13:19
    +2

    Картинки как вложения? Вы вообще ненормальные? Вложения к письмам — это прямой путь в чёрные списки. Не говоря уж о base64, когда в Gmail режутся письма больше 100кб. Если использовать все советы из статьи, гарантировано подучишь бан на домен или IP. Не надо так.


    1. akzhan
      17.12.2016 13:25
      -1

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


      Никаких проблем при этом со спамом не возникает.


      1. webirus
        17.12.2016 13:27

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


        1. akzhan
          17.12.2016 13:34

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


          И да, речи о массовых рассылках в статье нет.


          1. webirus
            17.12.2016 13:45

            Все, что отправляется с сайта, рано или поздно становится массовым. Речь не идёт о миллионных рассылках. Если вы отправляете 500+ писем в день, ваши письма начнут считать массовыми. Даже для самого мелкого проекта это небольшая цифра. И вот тогда начнутся большие проблемы со спам-фильтрами. Очень много умельцев было, что под видом вложений рассылали троянов. В связи с этим вложения стали чаще блокироваться, вместе с письмами. Внешние же ссылки, стали проходить сторонние антивирусные проверки. Вы отправляете письмо, Яндекс видит ссылки на картинки или вложения, отправляет их на проверку в DrWeb, и уже потом пересылает в ящик пользователя.


            1. akzhan
              17.12.2016 14:18

              Я работал какое-то время в REG.RU, и там у нас есть периодически рассылки по базе клиентов (много тысяч клиентов в день, внедренные изображения).


              В общем, никаких особых проблем нет абсолютно. Все решалось SPF, DKIM, разносом отправки на несколько IP-адресов. Никакой магии.


              1. webirus
                17.12.2016 14:31

                Возможно, когда-то и было. Но сейчас даже письма от того же REG.ru приходят без вложений. Я говорю про нынешнее положение дел, раньше может и прокатывало. А сейчас разнос писем на разные IP не даст толку, потому что спам-фильтры стали анализировать контент, а не только входные данные.


                1. akzhan
                  17.12.2016 14:37
                  +1

                  Да, смотрю, инлайнить изображения уже не требуется.


  1. eugin_b
    28.03.2017 15:59

    А такие подойдут?
    https://www.aliexpress.com/item/Powerful-Engine-JGB37-3530-12V-DC-Reversible-High-torque-Metal-37mm-Gearbox-Gearmotor/32667405760.html?spm=2114.01010208.3.84.151Jpf&ws_ab_test=searchweb0_0,searchweb201602_2_10065_10068_10136_10137_10060_10138_10062_10141_10056_10055_10054_301_10059_10099_10103_10102_10096_120_10144_10052_10053_10142_10107_10050_10143_10051_10526_10529_10084_10083_10119_10080_10082_10081_10110_10111_10112_10113_10114_10037_10517_10078_10079_10077_10073_10070_10122_10123_10120_10124,searchweb201603_1,afswitch_1_afChannel,ppcSwitch_2,single_sort_0_default&btsid=7561f0d9-221a-4215-be0e-5ebb53401033&algo_expid=fd4de230-840d-4244-95b3-63675ec64904-10&algo_pvid=fd4de230-840d-4244-95b3-63675ec64904

    там есть на 60 об/мин и н 12в., как вы заказывали


    1. akzhan
      17.12.2016 16:03
      +1

      Полезно посмотреть также https://developers.google.com/gmail/markup/


      1. k12th
        17.12.2016 16:07

        Я вообще редко имею дело с письмами, но пригодится, спасибо.