Принимаем электронную почту на Node.js

  • Tutorial

Электронная почта как и 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

    0
    Как обычно, нужная статья появляется уже после того, как сам наступил на все грабли. Но будем надеяться, другим она поможет.
      0
      Можно пару вопросов:
      1. Ключи от Let's Encrypt можно использовать те же, что и для 443 порта?
      2. Никакие пакеты, кроме spamassassin на сервер не требуется устанавливать?
      3. У DNS домена, какие должны быть MX записи?
      4. Работает только на приём писем?
        +1
        1. Да.
        2. Для тестирования может понадобится sendmail.
        3. Я выставил CNAME "mail.hm" "hm.rumk.in" и TXT "hm" "v=spf1 ip4:159.203.137.17 -all", MX не устанавливал. Yandex и Google исправно доставляют. Возможно это не правильно.
        4. Да. Для отправки используйте nodemailer от того же автора.
          0

          По RFC можно жить без MX, но не все почтовики нормально с этим работают, а то, что теряется часть писем можно заметить не сразу. Ещё желательна PTR в обратной зоне.

        0
        Тем временем разработчики не слишком-то ее ценят и используют в одностороннем порядке, указывая отправителем noreply. И в первую очередь это связано с трудоемкостью процесса обработки входящей корреспонденции.

        Теперь почта сама себя читать будет и отправлять ответы с того же noreplay.
        Какую проблему устраняет данное решение? Какие плюсы по сравнению с postfix или exim?
          0
          Теперь почта сама себя читать будет и отправлять ответы с того же noreplay.

          Очевидно, что уже не с noreply, а с более подходящего адреса. Например github позволяет отправлять ответы на комментарии через почту. Пространство для автоматизации – огромно.


          Данное решение снижает порог входа в технологии электронной почты и устраняет зависимости в виде Postfix и Exim.

            0

            Заменяя оные на nodejs, npm и smtp-server + mailparser с их зависимостями (ещё 10 пакетов).

          0
          Не проще ли было просто читать ящик через imap — в таком варианте нет зависимости от работоспособности models сервиса.
            +1

            Не проще, потому что так нет зависимости от работоспособности/доступности imap сервиса.

              0
              Для отправки вы такой же самописный сервис используете, который сам ищет почтовые сервера в целевых доменах, общается с ними по smtp и управляет очередью повторов и т.п.?
              Я к тому, что прикрутить imap к существующему postfix/exim надежнее и дешевле.
              Как вариант, можно ещё просто складывать почту существующим почтовым сервером в mailbox/maildir форматах и просто разбирать файлы.

              В таком раскладе не нужно реализовывать проверки spf, dkim и т.п. и в дальнейшем поддерживать этот код, решать проблемы безопасности.

              Да, отбойники большинство сервисов шлют или в формате https://tools.ietf.org/html/rfc3464 или используя заголовки X-Failed-Recipients
                0

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


                Я к тому, что прикрутить imap к существующему postfix/exim надежнее и дешевле.

                Практика показывает обратное. Иначе бы не плодились сервисы по отправке и получению почты как грибы. Сегодня содержать почту очень дорого.


                Как вариант, можно ещё просто складывать почту существующим почтовым сервером в mailbox/maildir

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


                В таком раскладе не нужно реализовывать проверки spf, dkim

                Ну, в случае с nodejs – разрабатываешь один раз и кладешь в npm. А если нет необходимости общаться со всем миром, а только с ограниченным кругом пользователей можно включить SMTP-авторизацию или использовать временные адреса вида {pretty_long_random_id}@host, которые защищены от подбора и по сути являются токенами.

                0
                переложить прием и отдачу почты на яндекс и гугл и читать с них куда как удобнее, чем самому отвечать за работоспособость smtp сервера
                  –1

                  Вы предлагаете болеутоляющее, а не лечение.


                  То что поддержка почты сегодня – слишком дорогое удовольствие говорит лишь о том, что потенциал этой сферы велик. К тому же ваш вариант страдает потерей конфиденциальности.

              –1
              Свой сервер в любом случае надежнее всяких yandex mail b gmail сервисов, как минимум потому что он находится на вашем железе под вашим контролем, а если и на ноде можно сделать так вообще супер стало
                0

                Падение скольких серверов из вашего одного не повлияет на работу почтовой системы? Подсказать ответ?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое