Введение
Почти в каждом проекте приходится думать об отправке писем по электронной почте. Основными требованиями при этом являются, помимо надежности доставки, привлекательность и удобство электронных писем.
Основные нюансы при формировании таких писем:
- Все стили должны встраиваться (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};
}
Полезные ссылки
- Исходный код проекта по данной статье.
- Подборка: 40+ полезных инструментов, ресурсов и исследований о работе с email
- MJML — Удобный препроцессор электронных писем со своим синтаксисом, включает в себя
gulp-mjml
. Спасибо Icewild за наводку. - Foundation for Emails 2. Ещё один препроцессор писем с расширенным HTML-синтаксисом и CSS-фреймворком. Спасибо A1MaZ за наводку.
P.S.: Спасибо pstn за доработки шаблонов писем.
Комментарии (22)
Icewild
17.12.2016 12:58В чем преимущества перед https://mjml.io/?
akzhan
17.12.2016 13:02Интересный язык разметки — MJML.
Надо казать, что это не замена, а удобный препроцессор, на замену тому же
gulp-pug
.
Благо предоставляется пакет
gulp-mjml
.
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.
webirus
17.12.2016 13:19+2Картинки как вложения? Вы вообще ненормальные? Вложения к письмам — это прямой путь в чёрные списки. Не говоря уж о base64, когда в Gmail режутся письма больше 100кб. Если использовать все советы из статьи, гарантировано подучишь бан на домен или IP. Не надо так.
akzhan
17.12.2016 13:25-1Обычно в письма вкладывают одну или две картинки, для придания узнаваемого стиля.
Никаких проблем при этом со спамом не возникает.
webirus
17.12.2016 13:27Вложения к письмам могут быть при написании одного-двух писем, как это делается с подписью и стилем. Но если речь о массовой рассылке, а статья именно для массовой, то вложения недопустимы.
akzhan
17.12.2016 13:34Начнем с того, что массовые рассылки лучше поручать отдельным компаниям, которые оными занимаются.
И да, речи о массовых рассылках в статье нет.
webirus
17.12.2016 13:45Все, что отправляется с сайта, рано или поздно становится массовым. Речь не идёт о миллионных рассылках. Если вы отправляете 500+ писем в день, ваши письма начнут считать массовыми. Даже для самого мелкого проекта это небольшая цифра. И вот тогда начнутся большие проблемы со спам-фильтрами. Очень много умельцев было, что под видом вложений рассылали троянов. В связи с этим вложения стали чаще блокироваться, вместе с письмами. Внешние же ссылки, стали проходить сторонние антивирусные проверки. Вы отправляете письмо, Яндекс видит ссылки на картинки или вложения, отправляет их на проверку в DrWeb, и уже потом пересылает в ящик пользователя.
akzhan
17.12.2016 14:18Я работал какое-то время в REG.RU, и там у нас есть периодически рассылки по базе клиентов (много тысяч клиентов в день, внедренные изображения).
В общем, никаких особых проблем нет абсолютно. Все решалось SPF, DKIM, разносом отправки на несколько IP-адресов. Никакой магии.
webirus
17.12.2016 14:31Возможно, когда-то и было. Но сейчас даже письма от того же REG.ru приходят без вложений. Я говорю про нынешнее положение дел, раньше может и прокатывало. А сейчас разнос писем на разные IP не даст толку, потому что спам-фильтры стали анализировать контент, а не только входные данные.
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в., как вы заказывали
A1MaZ
Посмотрите на http://foundation.zurb.com/emails.html
У них свои html теги, позволяют избавиться от миллиона вложенных таблиц в шаблонах. У меня в двух проектах с успехом работают, письма отображаются идеально на всех веб-клиентах и Apple системах. Есть небольшие проблемы с андроидом, огромные проблемы с аутлуком, как и всегда.
akzhan
Спасибо, интересное развитие Ink. Собственно, его CSS-часть вполне удобно использовать (Foundation for Emails 2) совместно.
Остальное чуть позже гляну.
webirus
Поэтому у нас в компании все ещё верстают руками, без инструментов. Зато письма смотрятся одинаково хорошо, как на телефонах, так и на компах, даже в аутлуке письма выглядят точно также, как на макете. Сборщики не могут учесть всех особенностей.
A1MaZ
Если не секрет, покажите скрин какого-то письма?
webirus
Для всеобщей публикации не подходящее письмо выбрал, может найду что-то другое. Но в ЛС отправил.
Radiocity
А как дела обстоят с мобильными клиентами мейла и яши?)
A1MaZ
Мы тестируем через litmus, там их нет, поэтому не знаю. Напрягу тестировщиков в понедельник, не подумал об этом, спасибо. Я на айфоне пользуюсь клиентом email, в нем все ок. Подозреваю, что в айфонах должно быть все ок, а с андроида у нас 4% входов всего.
A1MaZ
Проверили на иос сторонние клиенты. Яху немного поехал, но не критично, все остальные в норме. Разве что некоторые не смогли понять, что нужно показать мобильную версию, а показали десктопную.
Мейл не проверяли — у нас стартап для штатов и мейла ни у кого нет.
Плюс еще проблема в том, что у нас письма нарисовал какой-то крутой американский дизайнер, который жил в своем отдельном дизайнерском мире и проблемы верстки писем его совсем не волновали. Нам пришлось подстраиваться под него.