Sending e-mail with alternative parts plus attachments

Today I was working on my MailService component in a ZF2 project. For this specific project I needed to be able to support alternative parts for text and html combined with attachments. The alternative parts are to allow less sophisticated mail readers, not capable of rendering html, to still read the message.

When sending mail using the Zend\Mail\Message class you have to create a Zend\Mime\Message object and assign this object to the body of Zend\Mail\Message, using setBody(). The Zend\Mime\Message object contains the actual parts of the message and the Zend\Mail\Message is responsible for the mail headers.

The default content-type of a Zend\Mail\Message is ‘multipart/mixed’. This is usually perfect if you’re only sending one text part and optionally one or more attachments, but if you want to support alternative parts (requires content-type ‘multipart/alternate’) AND attachments (requires ‘multipart/mixed’) then you have a little problem.

See some sample code first of what a typical usage of the Zend\Mail and Zend\Mime classes looks like:

 

use Zend\Mail as ZendMail;
use Zend\Mail\Message as ZendMessage;
use Zend\Mime\Part as MimePart;
use Zend\Mime\Message as MimeMessage;

public function sendMailFromTemplate($to, $from, $fromName, $subject, $template, array $viewValues = array(), $attachments = array(), $bcc = null)
{
		// Render content from template
		$htmlContent = $this->viewRenderer->render($template, $viewValues);

		// Create HTML part
		$htmlPart = new MimePart($htmlContent);
		$htmlPart->type = "text/html";

		// Create plain text part
		$stripTagsFilter = new \Zend\Filter\StripTags();
		$textContent = str_ireplace(array("<br />", "<br>"), "\r\n", $htmlContent);
		$textContent = $stripTagsFilter->filter($textContent);
 		$textPart = new MimePart($textContent);
 		$textPart->type = "text/plain";

 		$body = new MimeMessage();
 		$body->setParts(array($textPart, $htmlPart));

		// Create mail message
		$mailMessage = new ZendMessage();
		$mailMessage->setFrom($from, $fromName);
		$mailMessage->setTo($to);
		$mailMessage->setSubject($subject);
		$mailMessage->setBody($body);
		$mailMessage->setEncoding("UTF-8");
		$mailMessage->getHeaders()->get('content-type')->setType('multipart/alternative');

		if ($bcc)
		{
			$mailMessage->addBcc($bcc);
		}

		$this->mailTransport->send($mailMessage);
	}

Please ignore the long list of parameters of this method, because it still needs some refactoring. Also ignore the rendering of the template code. I’m using the regular Zend\View\Renderer for rendering my e-mail content using templates.

So in the above code we create a $textPart and an $htmlPart and add those parts to $body (which is the Zend\Mime\Message object) and finally assign the $body to $mailMessage using setBody. Furthermore the content-type of $mailMessage is manually set to ‘multipart/alternative’ to support the alternative parts.

Now if we were to add attachments to this you walk into a problem:

        $body = new MimeMessage();
 		$body->setParts(array($textPart, $htmlPart));

		// Attachments
		if ($attachments && is_array($attachments) && count($attachments) > 0)
		{
			foreach ($attachments as $curAttachment)
			{
				$attachment = new MimePart( file_get_contents($curAttachment) );
				$attachment->type = \Zend\Mime\Mime::TYPE_OCTETSTREAM;
				$attachment->filename = basename($curAttachment);
				$attachment->disposition = \Zend\Mime\Mime::DISPOSITION_ATTACHMENT;

				$body->addPart($attachment);
			}
		}

The problem is that since the content-type of the message is still 'multipart/alternative' that the attachment is treated as one of the alternative parts, which isn't what we want of course.

This can be fixed in a pretty easy way, by creating a separate Zend\Mime\Message object to contain the alternative parts (text and html parts), add it as one part to the body and then add the attachment parts to the body. Finally change the content-type of the Zend\Mail\Message to 'multipart/mixed':
		// Render content from template
		$htmlContent = $this->viewRenderer->render($template, $viewValues);

		// Create HTML part
		$htmlPart = new MimePart($htmlContent);
		$htmlPart->type = "text/html";

		// Create plain text part
		$stripTagsFilter = new \Zend\Filter\StripTags();
		$textContent = str_ireplace(array("<br />", "<br>"), "\r\n", $htmlContent);
		$textContent = $stripTagsFilter->filter($textContent);
 		$textPart = new MimePart($textContent);
 		$textPart->type = "text/plain";

                // Create separate alternative parts object
 		$alternatives = new MimeMessage();
 		$alternatives->setParts(array($textPart, $htmlPart));
 		$alternativesPart = new MimePart($alternatives->generateMessage());
 		$alternativesPart->type = "multipart/alternative;\n boundary=\"".$alternatives->getMime()->boundary()."\"";

                $body = new MimeMessage();
 		$body->addPart($alternativesPart);

                // Attachments
		if ($attachments && is_array($attachments) && count($attachments) > 0)
		{
			foreach ($attachments as $curAttachment)
			{
				$attachment = new MimePart( file_get_contents($curAttachment) );
				$attachment->type = \Zend\Mime\Mime::TYPE_OCTETSTREAM;
				$attachment->filename = basename($curAttachment);
				$attachment->disposition = \Zend\Mime\Mime::DISPOSITION_ATTACHMENT;
				$attachment->encoding = \Zend\Mime\Mime::ENCODING_BASE64;

				$body->addPart($attachment);
			}
		}

                // Create mail message
		$mailMessage = new ZendMessage();
		$mailMessage->setFrom($from, $fromName);
		$mailMessage->setTo($to);
		$mailMessage->setSubject($subject);
		$mailMessage->setBody($body);
		$mailMessage->setEncoding("UTF-8");
		$mailMessage->getHeaders()->get('content-type')->setType('multipart/mixed');

		if ($bcc)
		{
			$mailMessage->addBcc($bcc);
		}

		$this->mailTransport->send($mailMessage);