1

我们公司基于 Zend(1.8.4 版本)开发了自己的 CMS。切换到新版本暂时是不可能的。

我们使用 Zend Mail 发送(多部分)带有嵌入图像 ( Content-Disposition: inline;) 和可下载附件 ( Content-Disposition: attachment;) 的消息。

几天前,一位客户报告在他的 Apple iPhone 5(内部邮件客户端)上打开此类邮件时遇到问题:在收件箱中,邮件确实标有一个符号,表示邮件有附件。但是,打开邮件后,附件是不可见的。当前版本的 Outlook、Thunderbird 和各种网络邮件客户端不存在该问题。

我通过根据附件的存在更改邮件的 Content-Type 来解决问题:

  • 邮件包含嵌入的图像和可下载的附件:Content-Type: multipart/mixed;
  • 邮件包含嵌入的图像,但没有可下载的附件:Content-Type: multipart/related;

我还必须更改有关不同部分边界组装的功能_buildBodyZend/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--

任何帮助表示赞赏。

亲切的问候,

尼尔斯

4

2 回答 2

4

这条消息似乎有点过时了,但我知道这个问题,因为我驱动一个更大的应用程序,深深植根于 zend FW 1。不幸的是,Zend_Mail 子系统在我看来在某些地方有点未进化。我认为像swiftmailer这样的 php 库比 ZF 1.12 甚至 ZF2 做得更好。因此,如果不能将您的邮件处理切换到 swiftmailer 并且并行使用ZF 1/ ZF 2,这似乎可以解决这个问题,但出于某些原因,您不希望您像我一样被困在 ZF1 中。

我发现就像 Tim 发布的那样,你的多部分消息的 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 代码(取自 v.1.12 Zend/Mail/Transport/Abstract.php)之后,您可以使用自己的结构构建消息。

我将为您举一个示例,说明带有内联图像和其他一些二进制附件的多部分消息的发布定义。我们需要的 mime 嵌套结构是

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

所以我们这样做

        // 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

注意缺少的第二个混合部分附件,我们现在只有一个部分,即多部分/替代。在这种情况下,ZF1 Mail 会做错,因为它是如此的概念化,以至于它只使用一个 Zend_Mime_Part(我的代码中的替代部分)作为 NON-multipart 邮件处理此配置,并从我们的硬组装 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 仅通过计算添加到 Zend_Mail 对象的部分来确定多部分,但在我们手动组装的情况下这是错误的)

结果是一封不符合您预期的邮件。

幸运的是,对于这个问题/情况有一个简单的解决方法,无需更改 ZF1。

只需在发送邮件之前将默认 Zend_Mail 标头 mime 从 multipart/mixed 更改为 multipart/alternative(只是我们的条带化部分标头)

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

现在邮件的周围部分已从混合变为替代,对于没有“真实”附件的情况来说这是绝对正确的。

对于所有其他情况,ZF1 本身会根据需要撰写邮件,因此通过处理这些注释,ZF1 v1.12 可以像其他优秀的电子邮件库一样处理复杂的电子邮件配置,并具有 ZF 集成的优势,这对谁来说是有益的。

于 2015-06-03T21:58:29.433 回答
3

我没有在 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 中绝对不可能。)

于 2013-10-21T15:43:57.040 回答