包含多个附件("attachment"和"inline")的多部分邮件与Zend Mail – RFC兼容?



我们公司开发了自己的基于Zend(版本1.8.4)的CMS。目前无法切换到新版本。

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

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

我通过根据附件的存在更改邮件的内容类型来解决此问题:

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

我还必须更改Zend/Mail/Transport/Abstract.php中关于不同零件边界组装的功能_buildBody

所以,我想知道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--

任何帮助,不胜感激。

亲切问候

尼尔斯

这条消息似乎有点过时了,但我知道这个问题,因为我用 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 的工作,并将内联图像与其他附件正确集成。但遗憾的是,您无法使用此结构构建消息在采埃孚 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/计划文本消息,如您在组装$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用于此)。

在这里我们进入第二个问题,或者采埃孚可能会将其视为功能,我不知道。但是例行公事中的部分发布了

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(); 

希望这有助于需要在更高级的情况下使用 ZF1 的邮件组件的人。

需要注意的一件重要事情:如果您处于某种情况下,您想在哪里仅将内联图像附加到您的邮件中,而不附加其他"真实"附件,ZF1会再次给你带来麻烦。我说的是这种情况:

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

注意缺少的第二个混合部件附件,我们现在只有一个部件,即多部分/替代方案。在这种情况下,ZF1 Mail 会做错,因为它的概念化使得它只用一个Zend_Mime_Part(我的代码中的替代部分)作为非多部分邮件来处理此配置,并剥离所需的来自我们硬组装Zend_Mime_Part对象的多部分/替代标头。(看看邮件/运输/摘要.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 从多部分/混合更改为多部分/替代(只是我们的条纹部分标头)您的邮件

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

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

对于所有其他情况,ZF1 会

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

我没有

在 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 中绝对是不可能的。

最新更新