Отслеживать и обрабатывать входящие письма в автоматическом режиме обычно нужно чтобы привязать их содержимое к какой-то сущности в базе данных, будь это тикет в тикетной системе вроде OTRS и Redmine, клиент или лид в CRM, или ответ сервера с ошибкой доставки письма в какой-то адрес из списка рассылки.

Наивные способы обработать письма, загрузив их по протоколу POP3 или через IMAP, рассматривать не будем: это тема другой статьи.

Что у нас есть?

Какие есть варианты поймать входящие письма:

  • Директива mailbox_command позволяет задать команду, которая будет вызываться для доставки писем локальным пользователям. Если письма этим же локальным пользователям нужно пересылать администратору или вебмастеру (например, отчёт из скриптов в cron), то такая директива не подойдёт. Например, это так если в /etc/aliases есть строчки для конкретных пользователей.

  • Можно перенаправлять письма в команду через пайп в /etc/aliases или аналогично через .forward в домашке. Вариант не подойдёт по тем же причинам что выше. Можно было бы завести отдельного пользователя или пользователя с тем же ID под забор почты, но это приведёт к или сложностям с раздачей прав, или к всевозможным странностям. Лучше наверняка.

  • Совсем все входящие письма можно просматривать указанием check_recipient_access с правилом FILTER внутри smtpd_recipient_restrictions. Этот директивы предназначены для смены получателя писем и удаления спама, а не для получения писем в адрес скрипта. Тоже относится к другим способам фильтрации писем. Это всё не для того.

  • Есть более экзотические варианты обработать входящие письма с помощью Dovecot или через хук в procmail. Это не то, о чём хотелось бы вспоминать через пару лет.

  • Письма в адрес конкретного получателя или целого поддомена можно переправить на обработку в любую программу посредством штатного для Postfix демона pipe.

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

Приступим

Простейшая программа на PHP для сохранения писем в каталог inbox может выглядеть так:

<?php
$email_content = stream_get_contents(STDIN);

if (!file_put_contents('inbox/'.date('Y-m-d_H-i-s_').sha1($email_content.strlen($email_content)), $email_content)) {
    echo "Failed to save message contents.\n";
    exit(1);
}

Скрипт сохраняет письма, поданные на стандартный вход, в файл с уникальным именем и префиксом из временной метки. Не должно быть никакой проблемы написать такую же программу на Python или Go.

Пусть полный путь до скрипта выглядит так:

/home/example/mailbox/save_email.php

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

Конфиг для демона pipe

С учётом вышесказанного, конфиг для запуска демона pipe с нашим скриптом будет выглядеть так:

inbox   unix  -       n       n       -       -       pipe
  -o soft_bounce=yes
  flags=F
  user=example
  directory=/home/example/mailbox/
  argv=php /home/example/mailbox/save_email.php ${sender} ${recipient}

Эти строки нужно добавить в master.cf, который обычно лежит в /etc/postfix.

Наш транспорт будет называться inbox — это же имя будет использоваться дальше при настройке Postfix.

На вход скрипту подаётся отправитель и получатель. Можно передавать и другие данные. Наш тестовый скрипт эти данные не использует. Кроме того, благодаря флагу F, перед сообщением будет добавлена строка вида From sender time_stamp.

Так как об ошибках при получении таких сообщений не следует уведомлять отправителя, включаем режим soft_bounce. В этом режиме при ошибках в работе скрипта сообщения будут возвращаться в очередь и Postfix будет пытаться передать их в скрипт повторно.

Настройка Postfix

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

postconf -e inbox_destination_recipient_limit=1

Наконец, нужно объяснить Postfix куда нужно обращаться для доставки писем в наш адрес. Для этого нужна таблица transport.

postconf -e transport_maps=hash:/etc/postfix/transport

Формат самого файла таблицы простой: домен или адрес, через пробел - назначение. Нужно иметь ввиду что для одного домена или адреса может быть только одно назначение. Например, если вы хотите чтобы входящая почта попадала не только в обработку в программу, но и во входящие человеку, то для внешнего адреса нужно сделать пересылку (через virtual_alias_maps) в другой внутренний адрес, письма на который уже будет уходить в программу.

Пример содержимого файла таблицы:

support@example.com inbox:
bounces.example.com inbox:

С таким содержимым все письма в адрес support@example.com и в любые адреса в поддомене bounces.example.com будут попадать на обработку в нашу программу. Это не обязательно должен быть одна программа: таких программ может быть несколько с разными именами.

После любых изменений в этом файле не забываем скомпилировать его в бинарный вид и перезагрузить Postfix.

postmap /etc/postfix/transport
postfix reload

Проверяем

Отправим тестовое письмо:

echo Test | mail -s "Test" support@example.com

Смотрим в /var/log/mail.log ход доставки сообщения:

postfix/pickup: uid=1010 from=<webmaster>
postfix/cleanup: message-id=<20171101221500.FOO001BAR@server.example.com>
postfix/qmgr: from=<webmaster@server.example.com>, size=424, nrcpt=1 (queue active)
postfix/pipe: to=<support@example.com>, relay=inbox, delay=0.09, status=sent (delivered via inbox service)
postfix/qmgr: removed

Проверим что файл появился:

ls -l /home/example/mailbox/inbox/*_*
cat /home/example/mailbox/inbox/*_*

Вот и всё.

Что дальше

Если нет особых требований к скорости обработки писем, а значит нет проблемы обрабатывать всю входящую почту раз в минуту отдельной программой в cron, то не стоит более усложнять программу. Это важно потому что ошибки из cron вы получите на почту, а ошибки из программы-обработчика писем пойдут в только почтовый лог в урезанном виде. Также стоит отметить что сама программа обработки письма не может выполняться больше чем указано в директиве command_time_limit, обычно это 1000 секунд.

Если нужно чтобы письма обрабатывались в реальном времени, то может иметь смысл отказаться от использования режима soft_bounce и перейти на явную передачу кодов ошибок. Для этого во всех ситуация вывод вашей программы должен начинаться с кода ошибки вида 4.X.X или 5.X.X.

Если хочется таким образом архивировать вообще всю почту, проходящую через Postfix, то для этого есть директива always_bcc.

Возможные ошибки

Настройки Postfix хоть и очень логичны, но не всегда бывает очевидно какая ошибка из-за конфликта каких директив возникает.

Recipient address rejected: User unknown in local recipient table

Такая ошибка может возникнуть в том числе если домен, в котором находится адрес, с которого письма должны поступать в обработку, указан в директиве mydestination. Тогда Postfix будет пытаться доставить письмо локально, и, не найдя получателя, не проверяет ничего дальше.

  • Одним решением будет убрать домен из списка или таблицы к этой директиве.
  • Другим решение будет добавить строку с одноименным пользователем в /etc/aliases c указание отправлять почту самому себе (напр. support: support@example.com). Так Postfix не будет сразу отказываться доставлять письма в этот адрес, а попробует доставить дальше, естественно найдя назначение в таблице transport_maps.