Multipart messages including multiple attachments (“attachment” and “inline”) with Zend Mail – RFC-compliant?

廉价感情. 提交于 2020-01-01 19:16:34

问题


Our company has developed its own CMS based on Zend (version 1.8.4). Switching to a new version is out of the question for the moment.

We are using Zend Mail for sending (multipart) messages with embedded images (Content-Disposition: inline;) and downloadable attachments (Content-Disposition: attachment;).

A few days ago, a customer reported problems opening such a mail on his Apple iPhone 5 (internal mail client): In the inbox the mail was indeed marked with a symbol indicating that the mail has attachments. However, after opening the mail, the attachment was not visible. The problem does not exist in the current versions of Outlook, Thunderbird and various webmail clients.

I fixed the problem by changing the Content-Type of the mail depending on the presence of attachments:

  • mail contains embedded images and downloadable attachments: Content-Type: multipart/mixed;
  • mail contains embedded images but no downloadable attachments: Content-Type: multipart/related;

I also had to change the function _buildBody in Zend/Mail/Transport/Abstract.php regarding the assembly of boundaries for the different parts.

So, I wonder if Zend Mail sends messages that are not RFC-compliant.

Here is the mail structure before (does not work with Apple Mail) and after (works in most common mail clients) adding my changes. Could you please tell me which version is RFC-compliant?

Zend Mail standard structure (not working with 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 customized structure (working in most common mail clients):

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

Any help is appreciated.

Kind regards,

Nils


回答1:


This message seems to be a bit outdated, but I know this problem, because I drive a bigger application deeply rooted with zend FW 1. Unfortunately Zend_Mail subsystem seems to me a bit unevolved on some places. I presume php libraries like swiftmailer do thisjob much better than ZF 1.12 and maybe even ZF2. So, if switching your mail processing to swiftmailer is not an option and parallel usage of ZF 1/ ZF 2, which seems to give solutions to this problem, is of some reasons not desired you are stuck within ZF1, just like me.

I found out that just like Tim posted, mime parts of your multipart message seem to be in wrong order and inclusion relation. I find this order/inclusion

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

would do a RFC-compliant work and correctly integrate inline images with other attachments. But sadly, you can't build messages with this structure in ZF 1.

The reason, why it is not possible is this part of code in ZF 1 (v1.12) file 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');
    }**

This routine assembles your Zend multipart Mail messages right before sending and does two things for no good. 1. is that it forces your body html/plan text message in a strict form, as you can see in the part, where $body is assembled. So there is no way to get your multipart/related section in this corset

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

since your only method in Zend_Mail to manipulate html body part do not allow you to use the multipart mime required instead of blank html text :

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

So, than one may think of doing stuff manually for your self and assemble your full multipart mail from singular parts (Zend_Mime_Part and Zend_Mime_Message are to use for this).

Here we come in to second problem, or ZF may see it as feature, i don't know. But the part in routine posted

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

prohibits a multipart mail configuration where you have not used calls to Zend_Mail::setBodyHtml and Zend_Mail::setBodyText. (In this case $body would be empty) If they are not set, an Error will be thrown and all your manually added Mime_Parts of your accurately assembled message, added with Zend_Mail::addPart(Zend_Mime_Part) will be simply ignored.

To get around this, you have to change behavior of the plotted routine to allow multipart messages without usage of setBodyHtml/setBodyText like this :

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');
    }

After this modification of ZF1 code (taken from v.1.12 Zend/Mail/Transport/Abstract.php) you are able to build messages with your own structure.

I will give you an example for posted definition of multipart message with inline images and some other binary attachments. Our required mime nesting structure is

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;
           }

Now you can send your manually assembled multi part mail as usual with an mail->send();

mail->send(); 

Hope this helps people that need to use mail component of ZF1 in more advanced situations.

One important thing to note: If you are in a situation, where you want to attach only inline images to your mail but no other "true" attachments, ZF1 will make you trouble again.. I'm talking of this situation :

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

Note the missing second mixed part attachment, we now have only one part, namely the multipart/alternative. In this situation, ZF1 Mail will do it wrong, because it is so concept-ed that it handles this configuration with only one Zend_Mime_Part (the alternative part from my code) as NON-multipart mail and strip the required multipart/alternative header from our hard assembled Zend_Mime_Part object. (Take a look at Mail/Transport/Abstract.php _send() routine

    $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;
    }

and in Zend/Mime/Message.php isMultiPart() and generateMessage(),

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

there you see the problem. Zend ZF1 determines multipart simply in counting added parts to Zend_Mail object, but this is wrong in our manually assembled situation)

Result is a mail that is not what you supposed.

Luckily there is an easy workaround for this problem/situation, without need to change ZF1.

Simply change the default Zend_Mail header mime from multipart/mixed to multipart/alternative (just our striped away part header) before sending your mail

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

Now surrounding part of mail has changed from mixed to alternative, what is absolutely correct for a situation with no "true" attachments.

For all other situations ZF1 natively does compose mail as required, so that with taking care of this notes, ZF1 v1.12 can handle complex email configurations just like other good email libraries and carries advantages of ZF integration for whom this is of benefit.




回答2:


I've not used multipart messages in ZF1, but I think this should be controlled by nesting of your message parts. From your description (and assuming you always want the PDF to be shown as an attachment), what you want is:

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

So your message contains two unrelated parts (multipart/mixed), being the message and the attached PDF. The message contains of two versions of the same thing (multipart/alternative), and those versions are the text version and the multipart/related version. The multipart related version contains the HTML message and its inline image.

If I'm reading your boundaries correctly (and there appears to be one missing), what you currently have is:

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

So it makes sense for strict mail clients to ignore the PDF, since the header (multipart/related) indicates that it is part of the main message (and is referred to inline within it).

It's hard to suggest whether this can be fixed in your code without seeing it but hopefully this will point you in the right direction.

(As an aside, this is definitely not possible in ZF2, which doesn't support nested multipart messages.)



来源:https://stackoverflow.com/questions/19497672/multipart-messages-including-multiple-attachments-attachment-and-inline-wi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!