среда, 4 декабря 2013 г.

Особенности работы /usr/sbin/sendmail в разных MTA

Сегодня дебажили отправку почты в PHP приложении и наткнулись на интересную особенность эмуляции ключей /usr/sbin/sendmail в различных MTA.

Предыстория - рассматриваемое приложение отправляет письмо пользователю (user@example.net), делает Bcc: bcccopy@example.com (для внутренних нужд) и ставит Return-Path: sales@example.com. Если не лезть в дебри ООП и наслоения классов, то нужный код на PHP выглядит примерно так:

$email_to = 'user@example.net';
$email_subject = 'subject';
$email_body = 'email body';
$email_additional_headers = 'Bcc: bcccopy@example.com';
$email_additional_params = '-f sales@example.com';
mail($email_to, $email_subject, $email_body, $email_additional_headers, $email_additional_params);

Но при отправке письма из приложения приходило 3 одинаковых письма на user@example.net, на bcccopy@example.com и на sales@example.com, но при этом Return-Path: не выставлялся и выглядел примерно так: Return-Path: <www-data@www.example.com>

Нужное значание Return-Path может не выставляться, если почтовый сервер не считает пользователя доверенным, либо в вызове /usr/sbin/sendmail нет ключа -f. Проверить просто

# su - www-data
$ cat > /tmp/email.txt <_EOF_
Bcc: <bcccopy@example.com>
From: "Sales" <sales@example.com>
To: "User Full Name" <user@example.net>
Subject: subject

email body
_EOF_
$ cat /tmp/email.txt | /usr/sbin/sendmail -t -i -f sales@example.com

После выполнения пришли два письма - на user@example.net и на bcccopy@example.com, Return-Path: при этом выставлен корректно. Следовательно проблема лежит выше MTA и нужно дебажить PHP приложение.

Когда мне нужно проверить взаимодействие PHP скрипта и системы я пользуюсь sleep() и strace. Для этого в нужный PHP скрипт в самое начало вставляется sleep(60); после чего выполняется нужное действие. При этом у меня есть 60 секунд, чтобы посмотреть в http://www.example.com/server-status PID нужного мне запроса и подключиться к процессу через strace.

# strace -f -o /tmp/php-trace.log -s 8192 -p <PID>

По истечению 60 секунд приложение начнет выполнять вызовы системы, которые будут записаны в лог файле /tmp/php-trace.log. Поскольку я дебажу отправку почты через mail(), то меня интересует вызов execve()

$ grep execve /tmp/php-trace.log 
3253  execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i  sales@example.com"], [/* 9 vars */] <unfinished ...>
3253  <... execve resumed> )            = 0
3254  execve("/usr/sbin/sendmail", ["/usr/sbin/sendmail", "-t", "-i", "sales@example.com"], [/* 9 vars */]) = 0
3255  execve("/usr/sbin/postdrop", ["/usr/sbin/postdrop", "-r"], [/* 2 vars */]) = 0

Вот теперь видно почему отправляется три письма вместо двух - где-то в приложении ошибка и перед sales@example.com не добавляется параметр -f. А раз sales@example.com указан без параметра, то он трактуется как еще один адрес назначения.

Но остается еще один вопрос - почему на некоторых серверах отправляется три письма, а на некоторых только два (правильный Return-Path не выставляется на любом сервере). Во всех случаях код приложения одинаковый и содержит ошибку.

На сервере, где пытались воспроизвести ошибку с отправкой трех писем вместо двух установлен WHM/cPanel и MTA там exim4. Поскольку код одинаковый, значит и функция mail() вызывается с одинаковыми параметрами, а значит и вызов /usr/sbin/sendmail должен быть одинаковый. На всякий случай сравниваю значение переменной sendmail_path в выводе phpinfo() на разных серверах - везде стоит одинаковое значение "/usr/sbin/sendmail -t -i". Но почему же тогда отличается поведение!

Смотрю дальше - на сервере, где отправляется только два письма стоит Exim4, а на остальных, где отправляется три письма, - Postfix. Сравниваю описание ключей -t и -i в man sendmail от этих MTA (взято из CentOS 6.5).

Exim4

-i        This option, which has the same effect as -oi, specifies that
          a dot on a line by itself should not terminate  an  incoming,
          non-SMTP message. I can find no documentation for this option
          in Solaris 2.4 Sendmail, but the mailx command in Solaris 2.4
          uses it. See also -ti.

-t        When  Exim is receiving a locally-generated, non-SMTP message
          on its standard input, the -t option causes the recipients of
          the message to be obtained from the To:, Cc:, and Bcc: header
          lines in the message instead of from the  command  arguments.
          The  addresses are extracted before any rewriting takes place
          and the Bcc: header line, if present, is then removed.

          If the command has any arguments, they specify  addresses  to
          which  the message is not to be delivered. That is, the argu-
          ment addresses are removed from the recipients list  obtained
          from  the  headers.  This  is  compatible with Smail 3 and in
          accordance with the documented behaviour of several  versions
          of Sendmail, as described in man pages on a number of operat-
          ing systems (e.g.  Solaris 8, IRIX 6.5, HP-UX  11).  However,
          some  versions  of  Sendmail  add argument addresses to those
          obtained from the headers, and  the  O'Reilly  Sendmail  book
          documents  it  that  way.  Exim  can  be made to add argument
          addresses instead of subtracting them by setting  the  option
          extract_addresses_remove_arguments false.

          If  there  are  any Resent- header lines in the message, Exim
          extracts recipients  from  all  Resent-To:,  Resent-Cc:,  and
          Resent-Bcc:  header lines instead of from To:, Cc:, and Bcc:.
          This is for  compatibility  with  Sendmail  and  other  MTAs.
          (Prior  to release 4.20, Exim gave an error if -t was used in
          conjunction with Resent- header lines.)

          RFC 2822 talks about different sets of Resent-  header  lines
          (for  when  a  message is resent several times). The RFC also
          specifies that they should be added at the front of the  mes-
          sage,  and  separated  by  Received:  lines. It is not at all
          clear how -t should operate in the present of multiple  sets,
          nor indeed exactly what constitutes a "set".  In practice, it
          seems that MUAs do not follow the RFC. The Resent- lines  are
          often  added  at  the  end of the header, and if a message is
          resent more than once, it is common for the original  set  of
          Resent-  headers to be renamed as X-Resent- when a new set is
          added. This removes any possible ambiguity.

Postfix

-i     When  reading  a message from standard input, don.t treat a line
       with only a . character as the end of input.

-t     Extract recipients from message headers. These are added to  any
       recipients specified on the command line.

       With Postfix versions prior to 2.1, this option requires that no
       recipient addresses are specified on the command line.

Sendmail

-i     Ignore  dots  alone on lines by themselves in incoming messages.
       This should be set if you are reading data from a file.

-t     Read message for recipients.  To:, Cc:, and Bcc: lines  will  be
       scanned  for recipient addresses.  The Bcc: line will be deleted
       before transmission.

Смысл опции -i примерно одинаков у всех MTA (хотя значение ignore у sendmail, еще нужно проверить - возможно он вовсе выкидывает строку, содержащую только символ точки). А вот после прочтения разделов про опцию -t все становится на свои места. На серверах где стоит exim4 используется отличное от sendmail и postfix поведение, когда получатели, указанные в командной строке, исключаются из итогового списка получателей письма.

Комментариев нет:

Отправить комментарий