Составные сообщения, включая несколько вложений (вложенных и встроенных) с Zend Mail - совместимы с RFC?

Наша компания разработала собственную CMS на базе Zend (версия 1.8.4). О переходе на новую версию пока не может быть и речи.

Мы используем Zend Mail для отправки (составных) сообщений со встроенными изображениями (Content-Disposition: inline;) и загружаемыми вложениями (Content-Disposition: attachment;).

Несколько дней назад клиент сообщил о проблемах с открытием такого письма на своем Apple iPhone 5 (внутренний почтовый клиент): в почтовом ящике письмо действительно было помечено символом, указывающим на то, что в письме есть вложения. Однако после открытия почты вложение не было видно. Эта проблема не существует в текущих версиях Outlook, Thunderbird и различных клиентов веб-почты.

Я исправил проблему, изменив Content-Type письма в зависимости от наличия вложений:

  • почта содержит встроенные изображения и загружаемые вложения: Content-Type: multipart/mixed;
  • почта содержит встроенные изображения, но не загружаемые вложения: Content-Type: multipart/related;

Мне также пришлось изменить функцию _buildBody в Zend/Mail/Transport/Abstract.php, касающуюся сборки границ для разных частей.

Итак, мне интересно, отправляет ли Zend Mail сообщения, не соответствующие RFC.

Вот структура почты до (не работает с Apple Mail) и после (работает в большинстве обычных почтовых клиентов) после добавления моих изменений. Подскажите, пожалуйста, какая версия RFC-совместима?

Стандартная структура Zend Mail (не работает с Apple Mail):

Content-Type: multipart/related; charset="utf-8"; boundary="=_0a0dbd2691e7728ea0f689fba0366bed"
MIME-Version: 1.0

--=_0a0dbd2691e7728ea0f689fba0366bed
Content-Type: multipart/alternative; boundary="=_a70ea5862a6842785870a9a4d003a2a7"
Content-Transfer-Encoding: 8bit

--=_a70ea5862a6842785870a9a4d003a2a7
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

[MAIL_TEXT]

--=_a70ea5862a6842785870a9a4d003a2a7
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

[MAIL_HTML]

--=_a70ea5862a6842785870a9a4d003a2a7--

--=_0a0dbd2691e7728ea0f689fba0366bed
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-ID: <test.pdf>
Content-Disposition: attachment; filename="test.pdf"

[PDF_ATTACHED]

Content-Type: image/jpeg
Content-Transfer-Encoding: base64
Content-ID: <test.jpg>
Content-Disposition: inline; filename="test.jpg"

[IMAGE_EMBEDDED]

--=_0a0dbd2691e7728ea0f689fba0366bed--

Настраиваемая структура Zend Mail (работает в большинстве распространенных почтовых клиентов):

Content-Type: multipart/mixed; charset="utf-8"; boundary="=_8ab337ec2e38e1a8b82a01a5712a8bdb"
MIME-Version: 1.0

--=_8ab337ec2e38e1a8b82a01a5712a8bdb
Content-Type: multipart/related; boundary="=_HTML60dd2cb7fc955f6c8a626c92c76aa2db"
Content-Transfer-Encoding: 8bit

--=_HTML60dd2cb7fc955f6c8a626c92c76aa2db
Content-Type: multipart/alternative; boundary="=_ALTd40db860af4718399b954c403d0b0557"
Content-Transfer-Encoding: 8bit

--=_ALTd40db860af4718399b954c403d0b0557
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

[MAIL_TEXT]

--=_ALTd40db860af4718399b954c403d0b0557
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

[MAIL_HTML]

--=_ALTd40db860af4718399b954c403d0b0557--

--=_HTML60dd2cb7fc955f6c8a626c92c76aa2db
Content-Type: image/jpeg
Content-Transfer-Encoding: base64
Content-ID: <gemeinschaft.jpg>
Content-Disposition: inline; filename="gemeinschaft.jpg"

[IMAGE_EMBEDDED]

--=_HTML60dd2cb7fc955f6c8a626c92c76aa2db--

--=_8ab337ec2e38e1a8b82a01a5712a8bdb
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-ID: <glamus-test-schnellwarnung.pdf>
Content-Disposition: attachment; filename="glamus-test-schnellwarnung.pdf"

[PDF_ATTACHED]

--=_8ab337ec2e38e1a8b82a01a5712a8bdb--

Любая помощь приветствуется.

С уважением,

Нильс


person MephKi    schedule 21.10.2013    source источник


Ответы (2)


Это сообщение кажется немного устаревшим, но я знаю эту проблему, потому что я использую более крупное приложение, глубоко укоренившееся на zend FW 1. К сожалению, подсистема Zend_Mail в некоторых местах кажется мне немного неразвитой. Я предполагаю, что библиотеки php, такие как swiftmailer, делают эту работу намного лучше, чем ZF 1.12 и, возможно, даже ZF2. Итак, если переключение обработки почты на swiftmailer не является вариантом и параллельным использованием ZF 1 / ZF 2, который, кажется, дает решения этой проблемы, по некоторым причинам нежелателен: вы застряли в ZF1, как и я.

Я обнаружил, что, как и написал Тим, части вашего составного сообщения mime, похоже, находятся в неправильном порядке и отношении включения. Я нахожу этот заказ / включение

multipart/mixed
multipart/alternative
    text/plain
    multipart/related
        text/html
        image/jpeg
application/pdf

будет выполнять работу в соответствии с RFC и правильно интегрировать встроенные изображения с другими вложениями. Но, к сожалению, вы не можете создавать сообщения с такой структурой в ZF 1.

Причина, по которой это невозможно, - это часть кода в файле ZF 1 (v1.12) Zend / Mail / Transport / Abstract.php

protected function _buildBody()
{
    if (($text = $this->_mail->getBodyText())
        && ($html = $this->_mail->getBodyHtml()))
    {
        // Generate unique boundary for multipart/alternative
        $mime = new Zend_Mime(null);
        $boundaryLine = $mime->boundaryLine($this->EOL);
        $boundaryEnd  = $mime->mimeEnd($this->EOL);

        $text->disposition = false;
        $html->disposition = false;

        **$body = $boundaryLine
              . $text->getHeaders($this->EOL)
              . $this->EOL
              . $text->getContent($this->EOL)
              . $this->EOL
              . $boundaryLine
              . $html->getHeaders($this->EOL)
              . $this->EOL
              . $html->getContent($this->EOL)
              . $this->EOL
              . $boundaryEnd;**

        $mp           = new Zend_Mime_Part($body);
        $mp->type     = Zend_Mime::MULTIPART_ALTERNATIVE;
        $mp->boundary = $mime->boundary();

        $this->_isMultipart = true;

        // Ensure first part contains text alternatives
        array_unshift($this->_parts, $mp);

        // Get headers
        $this->_headers = $this->_mail->getHeaders();
        return;
    }

    // If not multipart, then get the body
    if (false !== ($body = $this->_mail->getBodyHtml())) {
        array_unshift($this->_parts, $body);
    } elseif (false !== ($body = $this->_mail->getBodyText())) {
        array_unshift($this->_parts, $body);
    } 

    **if (!$body) {
        /**    
         * @see Zend_Mail_Transport_Exception
         */    
        require_once 'Zend/Mail/Transport/Exception.php';
        throw new Zend_Mail_Transport_Exception('No body specified');
    }**

Эта процедура собирает ваши составные почтовые сообщения Zend прямо перед отправкой и делает две бесполезные вещи. 1. это то, что он заставляет ваше тело html / plan текстовое сообщение в строгой форме, как вы можете видеть в той части, где собрано $ body. Таким образом, в этом корсете нет возможности получить свой составной / связанный раздел.

              . $boundaryLine
              . $html->getHeaders($this->EOL)
              . $this->EOL
              . $html->getContent($this->EOL)
              . $this->EOL
              . $boundaryEnd;

поскольку ваш единственный метод в Zend_Mail для управления частью тела html не позволяет использовать требуемый составной mime вместо пустого текста html:

setBodyHtml(string $html, string $charset = null, string $encoding = \Zend_Mime::ENCODING_QUOTEDPRINTABLE) : 

Итак, можно подумать о том, чтобы делать что-то вручную для себя и собирать полную составную почту из отдельных частей (для этого должны использоваться Zend_Mime_Part и Zend_Mime_Message).

Здесь мы подходим ко второй проблеме, или ZF может рассматривать это как особенность, я не знаю. Но часть в распорядке дня отправлена

if (!$body) {
        /**    
         * @see Zend_Mail_Transport_Exception
         */    
        require_once 'Zend/Mail/Transport/Exception.php';
        throw new Zend_Mail_Transport_Exception('No body specified');

запрещает составную почтовую конфигурацию, в которой вы не использовали вызовы Zend_Mail :: setBodyHtml и Zend_Mail :: setBodyText. (В этом случае $ body будет пустым). Если они не установлены, будет выдана ошибка, и все ваши вручную добавленные Mime_Parts вашего точно собранного сообщения, добавленные с помощью Zend_Mail :: addPart (Zend_Mime_Part), будут просто проигнорированы.

Чтобы обойти это, вы должны изменить поведение построенной процедуры, чтобы разрешить составные сообщения без использования setBodyHtml / setBodyText следующим образом:

if (!$body) {
        // this will probably only happen in multipart case 
        // where we need to assemble manually ..
        $this->_isMultipart = true;
         // set our manual headers :
        $this->_headers = $this->_mail->getHeaders();
        return; 

        /**    
         * @see Zend_Mail_Transport_Exception
         */    
        //require_once 'Zend/Mail/Transport/Exception.php';
        //throw new Zend_Mail_Transport_Exception('No body specified');
    }

После этой модификации кода ZF1 (взятого из Zend / Mail / Transport / Abstract.php v.1.12) вы можете создавать сообщения с вашей собственной структурой.

Я дам вам пример опубликованного определения составного сообщения со встроенными изображениями и некоторыми другими двоичными вложениями. Наша требуемая структура вложения мимов:

multipart/mixed
multipart/alternative
    text/plain
    multipart/related
        text/html
        image/jpeg
application/pdf

so we do

        // create a "multipart/alternative" wrapper
        $mailalternative = new Zend_Mime_Message();            
        // create a "multipart/related" wrapper
        $mailrelated     = new Zend_Mime_Message();

        // text/plain
        $mailplain        = new Zend_Mime_Part($textmail);
        $mailplain->encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE;
        $mailplain->type     = "text/plain; charset=UTF-8";

         // add it on right place
        $mailalternative->addPart($mailplain);

        // text/html            
        $mailhtml            = new  Zend_Mime_Part($htmlmail);
        $mailhtml->encoding  = Zend_Mime::ENCODING_QUOTEDPRINTABLE;
        $mailhtml->type      = "text/html; charset=UTF-8";

        // add it to related part
        $mailrelated->addPart($mailhtml);

        // try to add some inline img attachments
        $img_mimes = array('jpg'=>'jpeg','jpeg'=>'jpeg','png'=>'png');
        foreach($attachments as $attachment)
           if(isset($img_mimes[strtolower($attachment->Typ)]))
           {
              $suffix = strtolower($attachment->Typ);
              $at = new Zend_Mime_Part($attachment->doc_binary);
              $at->filename    = $attachment->doc_name.'.'.$attachment->Typ;
              $at->type        = 'image/'.$img_mimes[$suffix].'; name="'.$attachment->doc_name.'.'.$attachment->Typ.'"';
              $at->encoding    = Zend_Mime::ENCODING_BASE64;
              $at->disposition = Zend_Mime::DISPOSITION_INLINE;
              // id is important to address your pics in your html 
              // part later on. If id = XYZ you will write 
              // <img src="cid:XYZ"> in your html mail part ...
              $at->id          = $at->filename;
              // add them to related part, so they are accessible in html 
              $mailrelated->addPart($at);
           }

        $partrelated= new Zend_Mime_Part($mailrelated->generateMessage());
        $partrelated->type     = Zend_Mime::MULTIPART_RELATED;
        $partrelated->boundary = $mailrelated->getMime()->boundary();
        $mailalternative->addPart($partrelated);
        $partalternative = new Zend_Mime_Part($mailalternative->generateMessage());
        $partalternative->type = Zend_Mime::MULTIPART_ALTERNATIVE;
        $partalternative->boundary = $mailalternative->getMime()->boundary();
        // default mime type of zend multipart mail is multipart/mixed,
        // so here dont need to change type and simply set part:
        $mail->addPart($partalternative);


        // now try to add binary non inline attachments
        $img_mimes = array('jpg'=>'jpeg','jpeg'=>'jpeg','png'=>'png');
        foreach($attachments as $attachment)
        if(!isset($img_mimes[strtolower($attachment->Typ)]))
           {                            
              $at = $mail->createAttachment($attachment->doc_binary);
              $suffix = strtolower($attachment->Typ);
              $at->type = 'application/'.$suffix;                                                
              $at->filename = $attachment->doc_name.'.'.$attachment->Typ;
              $at->id       = $at->filename;
           }

Теперь вы можете отправлять собранные вручную письма, состоящие из нескольких частей, как обычно, с помощью mail-> send ();

mail->send(); 

Надеюсь, это поможет людям, которым необходимо использовать почтовый компонент ZF1 в более сложных ситуациях.

Следует отметить одну важную вещь: если вы находитесь в ситуации, когда вы хотите прикреплять к своей почте только встроенные изображения, но не прикреплять другие «настоящие» вложения, ZF1 снова вызовет у вас проблемы .. Я говорю об этой ситуации:

multipart/mixed
multipart/alternative
    text/plain
    multipart/related
        text/html
        image/jpeg

Обратите внимание на отсутствие вложения второй смешанной детали, теперь у нас есть только одна деталь, а именно multipart / alternate. В этой ситуации ZF1 Mail сделает это неправильно, потому что он так задуман, что обрабатывает эту конфигурацию только с одним Zend_Mime_Part (альтернативная часть из моего кода) как НЕ-составное письмо и удаляет требуемый составной / альтернативный заголовок из нашего жестко собранный объект Zend_Mime_Part. (Взгляните на подпрограмму Mail / Transport / Abstract.php _send ()

    $count    = count($this->_parts);
    $boundary = null;
    ...
    }

    if ($count > 1) {
        // Multipart message; create new MIME object and boundary
        $mime     = new Zend_Mime($this->_mail->getMimeBoundary());
        $boundary = $mime->boundary();
    } elseif ($this->_isMultipart) {
        // multipart/alternative -- grab boundary
        $boundary = $this->_parts[0]->boundary;
    }

а в Zend / Mime / Message.php isMultiPart () и generateMessage (),

    public function isMultiPart()
{
    return (count($this->_parts) > 1);
}

вот и проблема. Zend ZF1 определяет multipart просто путем подсчета добавленных частей к объекту Zend_Mail, но это неверно в нашей ситуации ручной сборки)

Результат - письмо, которое не то, что вы предполагали.

К счастью, для этой проблемы / ситуации есть простой способ обхода без изменения ZF1.

Просто измените mime заголовка Zend_Mail по умолчанию с multipart / mixed на multipart / alternate (только наш полосатый заголовок) перед отправкой вашей почты.

      if($attachcount == 0 && $inlinecount > 0)
          $mail->setType('multipart/alternative');
      $mail->send();

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

Для всех других ситуаций ZF1 изначально составляет почту по мере необходимости, так что с учетом этих примечаний ZF1 v1.12 может обрабатывать сложные конфигурации электронной почты, как и другие хорошие библиотеки электронной почты, и обладает преимуществами интеграции ZF, для которых это полезно.

person Pete Youlojoke    schedule 03.06.2015
comment
Пит, какой невероятно полезный патч и пошаговое руководство. Вы сэкономили мне много часов на переходе на swiftmailer. Спасибо!! - person Andy Fowler; 29.08.2018

Я не использовал составные сообщения в ZF1, но я думаю, что это должно контролироваться вложением частей вашего сообщения. Из вашего описания (и при условии, что вы всегда хотите, чтобы PDF-файл отображался как вложение), вы хотите:

multipart/mixed
    multipart/alternative
        text/plain
        multipart/related
            text/html
            image/jpeg
    application/pdf

Таким образом, ваше сообщение состоит из двух несвязанных частей (составных / смешанных): это сообщение и прикрепленный PDF-файл. Сообщение содержит две версии одного и того же (составной / альтернативной), и эти версии являются текстовой версией и составной / связанной версией. Версия, связанная с несколькими частями, содержит сообщение HTML и его встроенное изображение.

Если я правильно читаю ваши границы (и кажется, что одна отсутствует), то в настоящее время у вас есть:

multipart/related
    multipart/alternative
        text/plain
        text/html
    application/pdf
    image/jpeg

Таким образом, для строгих почтовых клиентов имеет смысл игнорировать PDF-файл, поскольку заголовок (составной / связанный) указывает, что он является частью основного сообщения (и упоминается внутри него).

Трудно предположить, можно ли это исправить в вашем коде, не видя этого, но, надеюсь, это укажет вам правильное направление.

(Кстати, это определенно невозможно в ZF2, который не поддерживает вложенные составные сообщения.)

person Tim Fountain    schedule 21.10.2013