Принимаем электронную почту на Node.js
Электронная почта как и 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,
});
// Валидация получателя. Для каждого адреса функция вызывается отдельно.
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.
: 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, чтобы избежать утечки памяти.
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": "Hi this is a test message. Notify me if you get it\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 ; 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 ",
"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.