Thursday, January 10, 2013

ZF2 Zend Mail, multipart/alternative and multipart/mixed Combined

Goals

The code included herein and in the Github repository is provided so you can use the code as-is, or you can learn how you can construct a multipart/alternative email with supporting attachments in Zend Framework 2. I do not describe all the code below, just giving some highlights of usage.

This was written against Zend Framework 2.0.6.

About

I am using Zend Framework 2 on an enterprise project at work. One of the requirements was to create a mail handler that all the application tiers would use as a central way of sending out mail. Rather than implement Zend Mail countless times throughout the entire codebase, I created a concrete class that would do most of the heavy lifting and provide a easier way of sending mail.

The mail object needed to send multipart/alternative emails in addition to supporting attachments that could be used as image cid references in html as well as normal attachments.

As of ZF 2.0.6, Zend Mail was not properly setting the multipart/alternative headers for the content-type of the messages. So the trick was, I needed to structure the boundaries appropriately so it would render the emails properly across various email clients.

There are two ways of doing this.
  1. Manually set the main content-type of the entire message as multpart/alternative.
  2. Keep the default multipart/mixed content-type that Zend creates as a default and create an additional boundary that is for the multipart/alternative.
I chose method (2). The main body of the message is multipart/mixed, because I generally have images and other attachments that are in the email, and I have an additional boundary that is specifically for the multipart/alternative. When I chose method (1), I had problems with some email clients. Some email clients were choosing to display the image attachments as the best alternative method rather than the html version. So with the embedded content-type boundary, this solved that problem.

The Code

I created a Github repository for the code sample found in this post. Review the full code for complete context.

In this code example, the functionality is made up of the php class and data transfer objects (DTO). The pattern is pretty straight forward: it's just a simple php class that is instantiated. The class uses the Sendmail transport by default and is not configurable. However you could easily change the class to work with a different transport or make it configurable if you need to manage multiple types of transports with appropriate setters.

Basically the process is this;
  • Html and text are added as Mime Parts to the message.
  • Attachments are added as Mime Parts, are encoded into base 64 and setup with an ID so they can be referenced in html emails.
  • The "send" method takes all the parts (addresses, attachments, html, text, subject, etc) and places them into boundaries and constructs the mail message and sends it through the transport.


Setting Up the Boundaries

The following code excerpt is from the "send" method and explains setting up multiple boundaries.
1:      $bodyMessage = new MimeMessage();  
2:    
3:      $multiPartContentMessage = new MimeMessage();  
4:    
5:      if ($this->text !== null) {  
6:        $multiPartContentMessage->addPart($this->text);  
7:      }  
8:    
9:      if ($this->html !== null) {  
10:        $multiPartContentMessage->addPart($this->html);  
11:      }  
12:    
13:      $multiPartContentMimePart = new MimePart($multiPartContentMessage->generateMessage());  
14:    
15:      $multiPartContentMimePart->type = 'multipart/alternative;' . PHP_EOL . ' boundary="' .  
16:        $multiPartContentMessage->getMime()->boundary() . '"';  
17:    
18:      $bodyMessage->addPart($multiPartContentMimePart);  
19:    
20:      foreach ($this->attachments as $attachment) {  
21:        $bodyMessage->addPart($attachment);  
22:      }  
23:    
24:      $zendMail->setBody($bodyMessage);  

First we create the first boundary on line 1. This is the root boundary that the other boundary will be nested in. The first boundary will also hold all the attachments in the email.

On line 3, we create the nested boundary which the multipart/alternative content will be placed.

Next, we add the html and text to the nested boundary as a Mime Part and then set that Mime Part's content type and boundary to the nested boundary and add the Mime Part to the root boundary.

Finally, we add each attachment (which is also a Mime Part) to the root boundary and set the mail body to the root boundary.

Adding Attachments

You can add multiple attachments to the mail message, including images that you want to reference with a "cid" for displaying in the mail message.

The method takes a DTO that describes the attachment you want to add. The DTO is self explanatory  Usage is something like:
1:  <?php  
2:  use ExampleZf2Mail\Mail\Dto\Attachment;  
3:  use ExampleZf2Mail\Mail\Mail;  
4:  $attachment = new Attachment();  
5:  $attachment->setBinary(file_get_contents('/path/to/file.png'))  
6:    ->setFileName('file.png');  
7:  $mail = new Mail();  
8:  $mail->addAttachment($attachment);  


Adding To/Cc/Bcc

Just like adding attachments, adding destinations also accepts a DTO. Usage is something like:
1:  use ExampleZf2Mail\Mail\Dto\Address;  
2:  use ExampleZf2Mail\Mail\Mail;  
3:  $address = new Address();  
4:  $address->setEmailAddress('user@domain.tld')  
5:    ->setName('Tom Jones');  
6:  $mail = new Mail();  
7:  $mail->addTo($address);  


Putting it All Together

And with everything together, including setting the html and text.
1:  <?php  
2:  use ExampleZf2Mail\Mail\Dto\Address;  
3:  use ExampleZf2Mail\Mail\Dto\Attachment;  
4:  use ExampleZf2Mail\Mail\Mail;  
5:    
6:  $html = <<<HTML  
7:  Hello this is my email.  
8:    
9:  <img src="cid:file1.png"/>  
10:  HTML;  
11:    
12:  $text = <<<TEXT  
13:  Hello this is my email.  
14:  TEXT;  
15:    
16:  $attachment1 = new Attachment();  
17:  $attachment1->setBinary(file_get_contents('file1.png'))  
18:    ->setFileName('file1.png');  
19:    
20:  $attachment2 = new Attachment();  
21:  $attachment2->setBinary(file_get_contents('file2.zip'))  
22:    ->setFileName('file2.zip');  
23:    
24:  $toAddress = new Address();  
25:  $toAddress->setEmailAddress('to@domain.tld')  
26:    ->setName('Tom Jones');  
27:    
28:  $fromAddress = new Address();  
29:  $fromAddress->setEmailAddress('from@domain.tld')  
30:    ->setName('Mary Jane');  
31:    
32:  $mail = new Mail();  
33:  $mail->addTo($toAddress)  
34:    ->setFrom($fromAddress)  
35:    ->setSubject('My Subject')  
36:    ->setText($text)  
37:    ->setHtml($html)  
38:    ->addAttachment($attachment1)  
39:    ->addAttachment($attachment2)  
40:    ->send();