Электронная почта как и www появилась на заре Интернета, и несмотря на свою архаичность продолжает удерживать позиции одной из главных технологий сети. Тем временем разработчики не слишком-то ее ценят и используют в одностороннем порядке, указывая отправителем noreply. И в первую очередь это связано с трудоемкостью процесса обработки входящей корреспонденции.
Тем временем, хвала комьюнити Node.js, появились пакеты, которые позволяют принимать почту без боли и страданий – это smtp-server и mailparser. Давайте я покажу, как в пару десятков строк кода создать свой почтовый сервер с поддержкой SSL шифрования, фильтрацией спама с помощь spamassassin и прочими радостями.
Получение писем
За получение писем отвечает модуль smtp-server. В его работе нет ничего сложного, единственное, что может заставить вас потратить несколько часов времени – это настройка TLS, которая сделана не слишком очевидной (позже я расскажу об этом).
const fs = require('fs');
const {SMTPServer} = require('smtp-server');
const smtp = new SMTPServer({
secure: false,
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem'),
onRcptTo,
onData,
authOptional: true,
});
// Валидация получателя. Для каждого адреса функция вызывается отдельно.
function onRcptTo({address}, session, callback) {
if (address.starts('noreply@')) {
callback(new Error(`Address ${address} is not allowed receiver`));
}
else {
callback();
}
}
// Обработка данных письма
function onData(stream, session, callback) {
// Stream – Поток с данными письма. Callback вызывается по окончанию парсинга.
// в этом обработчике мы будем парсить письмо.
callback();
}
Настройки
Вообще настроек несколько больше, но я опишу основные, которые понадобятся нам для реализации небольшого сервера.
secure
Дело в том что шифрование может использоваться двумя способами: при установке подключения (secure: true
) или с переключением на зашифрованный поток с помощью заголовка STARTTLS (secure: false
). Если вы слушаете 25-й порт, укажите false
, 587-й (465-й) – true
. Что бы определиться с портом советую прочесть статью mailgun про историю портов закрепленных за почтовыми протоколами.
key, cert
Ключ и сертификат SSL. По-умолчанию smtp-server использует собственный самоподписанный сертификат, но я бы не советовал его использовать, когда есть Let's Encrypt.
onRcptTo
Если в методе onRcptTo не был одобрен ни один адресс – onData вызыван не будет. Для каждого письма будет генерирован отчет на стороне отправителя. Яндекс генерирует вот это:
This is the mail system at host yandex.ru.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
Please, do not reply to this message.
<noreply@hm.rumk.in>: host hm.rumk.in[159.203.137.17] said: 550 Mailbox
noreply@hm.rumk.in could not receive messages (in reply to RCPT TO command)
onMailFrom
Эта настройка позволяет назначить обработчик для адреса отправителя для фильтрации по отправителям.
onData
Здесь все просто, главное вызвать callback, чтобы избежать утечки памяти.
authOptional
Позволяет принимать почту от неавторизованного отправителя, например Яндекса или Gmail.
logger
Может быть true
или экземпляром логгера поддерживающего интерфейс bunyan.
Парсинг
С парсингом все проще. Необходимо подключить парсер для писем mailparser
:
const {MailParser} = require('mailparser');
и доработать функцию onData
:
function onData(stream, session, callback) {
const parser = new MailParser();
stream.pipe(parser);
parser.on('error', callback);
parser.on('end', (mail) => {
// Process mail body...
callback();
});
}
В результате парсинга вы получите объект вот такого вида:
{
"html": "<div>Hi this is a test message. Notify me if you get it</div>\n",
"headers": {
"received": [
"from mxback5g.mail.yandex.net (mxback5g.mail.yandex.net [77.88.29.166]) by forward17p.cmail.yandex.net (Yandex) with ESMTP id 372CD212FE for <c28ec25d@hm.rumk.in>; Sat, 5 Nov 2016 06:22:23 +0300 (MSK)",
"from web20g.yandex.ru (web20g.yandex.ru [95.108.253.229]) by mxback5g.mail.yandex.net (nwsmtp/Yandex) with ESMTP id j2CjR0Q3Ek-MN2SfLo3; Sat, 05 Nov 2016 06:22:23 +0300",
"by web20g.yandex.ru with HTTP; Sat, 05 Nov 2016 06:22:23 +0300"
],
"from": "Some User <user@host>",
"to": "c28ec25d@hm.rumk.in",
"subject": "asdasd a",
"mime-version": "1.0",
"message-id": "<7119991478316143@web20g.yandex.ru>",
"x-mailer": "Yamail [ http://yandex.ru ] 5.0",
"date": "Sat, 05 Nov 2016 06:22:23 +0300",
"content-transfer-encoding": "7bit",
"content-type": "text/html"
},
"subject": "Test message",
"messageId": "7119991478316143@web20g.yandex.ru",
"priority": "normal",
"from": [
{
"address": "user@host",
"name": "Some User"
}
],
"to": [
{
"address": "c28ec25d@hm.rumk.in",
"name": ""
}
],
"date": "2016-11-05T03:22:23.000Z",
"receivedDate": "2016-11-05T03:22:23.000Z"
}
Так же мы можем подключить модуль spamassassin для подсчета индекса "спамовости" spamScore
. Для этого понадобится установить spamassassin и модуль spamc-stream. Использовать так же легко как и mailparser.
Для этого понадобится установить и запустить spamassassin:
# Debian/Ubuntu
$ sudo apt-get install spamassassin
# Fedora/CentOS
$ sudo yum install spamassassin
Spamassassin содержит набор правил каждое из которых применяется к письму, и, если правило сработало, то индекс увеличивается. Когда индекс превышает допустимое значение (обычно 5), письмо признается спамом. Так например, индекс увеличится, если письмо содержит только html-версию без текстовой. Spamassassin это сервер, в который перенаправляется письмо для анализа. Smapc – клиент для smapassassin. Мы будем перенаправлять письмо сначала в spamassassin, а затем в парсер.
const SpamcStream = require('spamc-stream');
const spamc = new SpamcStream(); // Экземляр клиента
onData(stream, session, callback) {
const reporter = spamc.report();
let report;
const parser = new MailParser();
stream.pipe(reporter).pipe(mailparser);
reporter.on('report', (result) => {
report = result;
});
parser.on('end', (mail) => {
if (report.isSpam) {
// Save mail into spam directory
}
else {
// Process mail body...
}
callback();
});
reporter.on('error', callback);
parser.on('error', callback);
}
Так же следует отметить, что парсер писем умеет создавать потоки из аттачментов, что позволяет удобно и эффективно перенаправлять их в хранилища BLOB' ов, ну или просто писать на диск.
Примечание
Если вы решите принимать почту от неограниченного числа отправителей, вам понадобится реализовать поддержку проверки SPF и, желательно, DKIM. Но это материал для отдельной статьи.
Пример
Посмотреть как это работает вы можете на тестовой странице. Отправив письмо на временный e-mail, вы увидете JSON-структуру готовую для дальнейшей обработки. Сообщения доставляются в реальном времени по WebSocket. Исходники самого примера выложены в репозитории rumkin/hypemail.
Автором сервера и парсера является Андрис Райнман (Andris Reinman) поддержите проекты коммитами.
Комментарии (15)
botaniQQQ
13.11.2016 16:08Можно пару вопросов:
1. Ключи от Let's Encrypt можно использовать те же, что и для 443 порта?
2. Никакие пакеты, кроме spamassassin на сервер не требуется устанавливать?
3. У DNS домена, какие должны быть MX записи?
4. Работает только на приём писем?rumkin
13.11.2016 16:29+1- Да.
- Для тестирования может понадобится sendmail.
- Я выставил CNAME "mail.hm" "hm.rumk.in" и TXT "hm" "v=spf1 ip4:159.203.137.17 -all", MX не устанавливал. Yandex и Google исправно доставляют. Возможно это не правильно.
- Да. Для отправки используйте nodemailer от того же автора.
grossws
14.11.2016 12:29По RFC можно жить без MX, но не все почтовики нормально с этим работают, а то, что теряется часть писем можно заметить не сразу. Ещё желательна PTR в обратной зоне.
ckpunT
13.11.2016 16:45Тем временем разработчики не слишком-то ее ценят и используют в одностороннем порядке, указывая отправителем noreply. И в первую очередь это связано с трудоемкостью процесса обработки входящей корреспонденции.
Теперь почта сама себя читать будет и отправлять ответы с того же noreplay.
Какую проблему устраняет данное решение? Какие плюсы по сравнению с postfix или exim?rumkin
13.11.2016 17:16Теперь почта сама себя читать будет и отправлять ответы с того же noreplay.
Очевидно, что уже не с noreply, а с более подходящего адреса. Например github позволяет отправлять ответы на комментарии через почту. Пространство для автоматизации – огромно.
Данное решение снижает порог входа в технологии электронной почты и устраняет зависимости в виде Postfix и Exim.
grossws
14.11.2016 12:47Заменяя оные на
nodejs
,npm
иsmtp-server
+mailparser
с их зависимостями (ещё 10 пакетов).
RaveNoX
13.11.2016 17:06Не проще ли было просто читать ящик через imap — в таком варианте нет зависимости от работоспособности models сервиса.
rumkin
13.11.2016 17:25+1Не проще, потому что так нет зависимости от работоспособности/доступности imap сервиса.
RaveNoX
13.11.2016 17:38Для отправки вы такой же самописный сервис используете, который сам ищет почтовые сервера в целевых доменах, общается с ними по smtp и управляет очередью повторов и т.п.?
Я к тому, что прикрутить imap к существующему postfix/exim надежнее и дешевле.
Как вариант, можно ещё просто складывать почту существующим почтовым сервером в mailbox/maildir форматах и просто разбирать файлы.
В таком раскладе не нужно реализовывать проверки spf, dkim и т.п. и в дальнейшем поддерживать этот код, решать проблемы безопасности.
Да, отбойники большинство сервисов шлют или в формате https://tools.ietf.org/html/rfc3464 или используя заголовки X-Failed-Recipientsrumkin
13.11.2016 18:48В данном случае речь идет о получении. Не будем углубляться в отправку, это отдельная тема. Но вообще я использую модуль, который сам ищет сервера и отправляет почту. А написание всяких очередей я очень люблю, поэтому пишу сам, главное, что потом все отчеты можно вывести через админский интерфейс, привязать к нужному аккаунту/действию/событию.
Я к тому, что прикрутить imap к существующему postfix/exim надежнее и дешевле.
Практика показывает обратное. Иначе бы не плодились сервисы по отправке и получению почты как грибы. Сегодня содержать почту очень дорого.
Как вариант, можно ещё просто складывать почту существующим почтовым сервером в mailbox/maildir
И создать ненужную нагрузку на диск. Используя, nodejs я могу распараллелить обработку почты на несколько потоков, передать полученные письма внутренним серверам, там распарсить, обработать и все это без обращения к жесткому диску.
В таком раскладе не нужно реализовывать проверки spf, dkim
Ну, в случае с nodejs – разрабатываешь один раз и кладешь в npm. А если нет необходимости общаться со всем миром, а только с ограниченным кругом пользователей можно включить SMTP-авторизацию или использовать временные адреса вида {pretty_long_random_id}@host, которые защищены от подбора и по сути являются токенами.
erlyvideo
13.11.2016 18:48переложить прием и отдачу почты на яндекс и гугл и читать с них куда как удобнее, чем самому отвечать за работоспособость smtp сервера
rumkin
13.11.2016 19:10-1Вы предлагаете болеутоляющее, а не лечение.
То что поддержка почты сегодня – слишком дорогое удовольствие говорит лишь о том, что потенциал этой сферы велик. К тому же ваш вариант страдает потерей конфиденциальности.
antonsr98
14.11.2016 03:50-1Свой сервер в любом случае надежнее всяких yandex mail b gmail сервисов, как минимум потому что он находится на вашем железе под вашим контролем, а если и на ноде можно сделать так вообще супер стало
grossws
14.11.2016 12:49Падение скольких серверов из вашего одного не повлияет на работу почтовой системы? Подсказать ответ?
Sirion
Как обычно, нужная статья появляется уже после того, как сам наступил на все грабли. Но будем надеяться, другим она поможет.