Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
95.00% covered (success)
95.00%
19 / 20
CRAP
95.97% covered (success)
95.97%
119 / 124
AclUser\Mail\MailMessage
0.00% covered (danger)
0.00%
0 / 1
95.00% covered (success)
95.00%
19 / 20
43
95.97% covered (success)
95.97%
119 / 124
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setSmtpTransport
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
9 / 9
 setViewScript
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setLayoutTemplate
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setViewParams
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setInlineImages
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 setLayoutImages
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 setAttachments
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 embedImageFromSrc
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 validateAttachmentFormat
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
6 / 6
 getTransport
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 sendEmailBasedOnViewScript
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 addInlinePartsToMimeMessage
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
18 / 18
 addLayoutImages
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 addInlineImages
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 addAttachments
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 getImagesToEmbedFromSrc
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
10 / 10
 completeEmbedImagesFromSrc
0.00% covered (danger)
0.00%
0 / 1
3.65
58.33% covered (warning)
58.33%
7 / 12
 tryToGetImageFromSrc
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 createMimePart
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
<?php
/**
 * Class MailMessage
 *
 * @package     AclUser\Mail
 * @author      Nigel Hurnell
 * @version     v.1.0.0
 * @license     BSD
 * @copyright   Copyright (c) 2017, Nigel Hurnell
 */
namespace AclUser\Mail;
use Zend\Mail\Message as ZendMailMessage;
use Zend\View\Renderer\PhpRenderer;
use Zend\View\Model\ViewModel;
use Zend\Mime\Message as MimeMessage;
use Zend\Mime\Mime;
use Zend\Mime\Part as ZendMimePart;
use Zend\Mail\Transport\Sendmail;
use Zend\Mail\Transport\Smtp;
use Zend\Dom\Query;
/**
 * Class MailMessage send html mail messages based on view script
 * 
 * @package     AclUser\Mail
 * @author      Nigel Hurnell
 * @version     v.1.0.0
 * @license     BSD
 * @copyright   Copyright (c) 2017, Nigel Hurnell
 */
class MailMessage extends ZendMailMessage
{
    /**
     * The PhpRender used to render view script
     * 
     * @var PhpRenderer
     */
    protected $renderer;
    /**
     * Transport used to send message
     * 
     * @var Zend\Mail\Transport\TransportInterface 
     */
    protected $transport;
    /**
     * The view script used for the e-mail message
     * 
     * @var string 
     */
    protected $viewScript;
    /**
     * Keep track of the number of inline images so that the correct mime type can be assigned
     * 
     * @var integer the number of inline images
     */
    protected $totalImages = 0;
    /**
     * The layout script used for this e-mail message
     * 
     * @var string default layout template
     */
    protected $layoutTemplate = 'layout/email-layout';
    /**
     * An array of key - value pairs to be inserted into the view script
     * 
     * @var array 
     */
    protected $viewParams = [];
    /**
     * Array of inline images with image type and file location to be added the the e-mail view
     * 
     * @var array 
     */
    protected $inlineImages = [];
    /**
     * Array of inline images with image type and file location to be added the the e-mail layout
     * 
     * @var array 
     */
    protected $layoutImages = [];
    /**
     * Array of attachments to be added to the message 
     * Tested with png, jpg, pdf and zip files
     * 
     * @var array 
     */
    protected $attachments = [];
    /**
     * Whether images should be embedded in the e-mail based on the IMG tag SRC attribute
     * IMG tag needs to have the class embed-image for this to take effect
     * 
     * @var boolean 
     */
    protected $embedImageFromSrc = false;
    /**
     * integer to avoid errors in CID value
     * 
     * @var integer 
     */
    protected $index = 1;
    /**
     * Constructor (requires ZF phprenderer)
     * 
     * @param PhpRenderer $renderer used to render view script
     */
    public function __construct(PhpRenderer $renderer)
    {
        $this->renderer = $renderer;
    }
    /**
     * Set transport using SMTP protocol
     * 
     * @param Zend\Mail\Transport\Smtp $transport transport used to send e-mail
     */
    public function setSmtpTransport(Smtp $transport)
    {
        $config = $transport->getOptions()->getConnectionConfig();
        if (array_key_exists('username', $config) && array_key_exists('sender',
                        $config)) {
            $this->setFrom($config['username'], $config['sender']);
        } else if (array_key_exists('username', $config)) {
            $this->setFrom($config['username'], $config['username']);
        } else {
            return;
        }
        $this->transport = $transport;
    }
    /**
     * Set the view script that should be used for this e-mail message
     * 
     * @param string $viewScript pointer to location of view script
     * @return $this AclUser\Mail\MailMessage 
     */
    public function setViewScript($viewScript)
    {
        $this->viewScript = $viewScript;
        return $this;
    }
    /**
     * Over ride default e-mail layout
     * 
     * {@source} 
     * @param string $layoutTemplate
     * @return $this AclUser\Mail\MailMessage 
     */
    public function setLayoutTemplate($layoutTemplate)
    {
        $this->layoutTemplate = $layoutTemplate;
        return $this;
    }
    /**
     * Set view params to be used within the e-mail message view script
     * 
     * @param array $viewParams key value pairs to be passed to view script
     * @return $this AclUser\Mail\MailMessage 
     * {@source}
     */
    public function setViewParams($viewParams)
    {
        $this->viewParams = $viewParams;
        return $this;
    }
    /**
     * Set inline attachments (of view script) to send with message
     * the key of each array must correspond to parameter passed to the view script
     * each item in array must contain array with keys type and filepath
     * that corresponds to parameters of Zend\Mime\Part parameters of same name
     * 
     * @source 
     * 
     * @param array $inlineImages  array in format given above
     * @return $this AclUser\Mail\MailMessage 
     * @throws \Exception if 
     */
    public function setInlineImages($inlineImages)
    {
        if (!$this->validateAttachmentFormat($inlineImages)) {
            throw new \Exception("inline images array not in correct format");
        }
        $this->inlineImages = $inlineImages;
        return $this;
    }
    /**
     * Specify inline images to be added to layout script 
     * the key of each array must correspond to the parameter passed to e-mail layout
     * each item in array must contain array with keys: type and filepath
     * that corresponds to Zend\Mime\Part parameters of same name.
     * 
     * Exception will be thrown if array's requirements are not met
     * 
     * @param array $layoutImages array in format given above
     * @return $this AclUser\Mail\MailMessage 
     * @throws \Exception 
     */
    public function setLayoutImages($layoutImages)
    {
        if (!$this->validateAttachmentFormat($layoutImages)) {
            throw new \Exception("layout images array not in correct format");
        }
        $this->layoutImages = $layoutImages;
        return $this;
    }
    /**
     * Set (non inline) attachments to send with message
     * each item in array must contain array with keys type and filepath
     * that corresponds to parameters of Zend\Mime\Part parameters of same name
     * TYPES:
     * PDF - application/pdf
     * ZIP - application/octet-stream
     * PNG - image/png
     * JPG - image/jpg
     * 
     * @param array $attachments array of attachments
     * @return $this AclUser\Mail\MailMessage 
     * @throws \Exception
     */
    public function setAttachments($attachments)
    {
        if (!$this->validateAttachmentFormat($attachments)) {
            throw new \Exception("attachment array not in correct format");
        }
        $this->attachments = $attachments;
        return $this;
    }
    /**
     * Tell system whether to embed images based on IMG tag SRC attribute
     * note that the IMG tag must have a class of ember-image as well
     * 
     * @param boolean $embedImageFromSrc whether to search image src and embed images where possible
     * @return $this AclUser\Mail\MailMessage 
     */
    public function embedImageFromSrc($embedImageFromSrc = true)
    {
        $this->embedImageFromSrc = $embedImageFromSrc;
        return $this;
    }
    /**
     * Check whether images array passed to set methods has the correct format
     * 
     * @param array $attachments 
     * @return boolean
     */
    protected function validateAttachmentFormat($attachments)
    {
        $result = true;
        foreach ($attachments as $attachment) {
            if (!array_key_exists('type', $attachment) || !array_key_exists('filepath',
                            $attachment)) {
                $result = false;
            }
        }
        return $result;
    }
    /**
     * Get the transport to be used to send the message
     * 
     * @return Zend\Mail\Transport\TransportInterface
     */
    protected function getTransport()
    {
        if (!isset($this->transport)) {
            return new Sendmail();
        }
        return $this->transport;
    }
    /**
     * Send the e-mail message based on the view script
     */
    public function sendEmailBasedOnViewScript()
    {
        $mimeMessage = $this->addAttachments($this->addInlinePartsToMimeMessage(new MimeMessage()));
        $this->setBody($mimeMessage);
        $mimeType = 0 === $this->totalImages ? Mime::TYPE_HTML : Mime::MULTIPART_MIXED;
        $this->getHeaders()->get('Content-Type')->setType($mimeType);
        $transport = $this->getTransport();
        $transport->send($this);
    }
    /**
     * Add all inline elements to the e-mail message
     * 
     * @param Zend\Mime\Message $mimeMessage
     * @return Zend\Mime\Message
     */
    protected function addInlinePartsToMimeMessage(MimeMessage $mimeMessage)
    {
        $view = new ViewModel($this->viewParams);
        if (!isset($this->viewScript)) {
            throw new \Exception("You must set the view script");
        }
        $view->setTemplate($this->viewScript);
        $this->addInlineImages($mimeMessage, $view);
        $content = $this->renderer->render($view);
        $viewLayout = new ViewModel();
        $viewLayout->setTemplate($this->layoutTemplate)
                ->setVariable('content', $content);
        $this->addLayoutImages($mimeMessage, $viewLayout);
        $rendered = $this->getImagesToEmbedFromSrc($this->renderer->render($viewLayout),
                $mimeMessage);
        $htmlMimePart = new ZendMimePart($rendered);
        $htmlMimePart->type = Mime::TYPE_HTML;
        $htmlMimePart->charset = 'utf-8';
        $htmlMimePart->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
        $mimeMessage->addPart($htmlMimePart);
        return $mimeMessage;
    }
    /**
     * Actually add the inline images to the layout of the e-mail message
     * 
     * @param Zend\Mime\Message $mimeMessage
     * @param ViewModel $viewLayout
     */
    protected function addLayoutImages(MimeMessage $mimeMessage, $viewLayout)
    {
        foreach ($this->layoutImages as $key => $layoutImage) {
            $image = $this->createMimePart(file_get_contents($layoutImage['filepath']),
                    basename($layoutImage['filepath']), $layoutImage['type'],
                    Mime::DISPOSITION_INLINE);
            $mimeMessage->addPart($image);
            $viewLayout->setVariable($key, $image->id);
            $this->totalImages++;
        }
    }
    /**
     * Actually add the inline images to the view of the e-mail message
     * 
     * @param Zend\Mime\Message $mimeMessage
     * @param ViewModel $view
     */
    protected function addInlineImages(MimeMessage $mimeMessage, $view)
    {
        foreach ($this->inlineImages as $key => $inlineImage) {
            $image = $this->createMimePart(file_get_contents($inlineImage['filepath']),
                    basename($inlineImage['filepath']), $inlineImage['type'],
                    Mime::DISPOSITION_INLINE);
            $mimeMessage->addPart($image);
            $view->setVariable($key, $image->id);
            $this->totalImages++;
        }
    }
    /**
     * Actually add the attachments of the e-mail message
     * 
     * @param Zend\Mime\Message $mimeMessage
     */
    protected function addAttachments(MimeMessage $mimeMessage)
    {
        foreach ($this->attachments as $attachment) {
            $emailAttachment = $this->createMimePart(
                    file_get_contents($attachment['filepath']),
                    basename($attachment['filepath']), $attachment['type'],
                    Mime::DISPOSITION_ATTACHMENT);
            $mimeMessage->addPart($emailAttachment);
        }
        return $mimeMessage;
    }
    /**
     * Search through IMG tags and embed them if embedImageFromSrc evaluates to true 
     * and the IMG tag has the embed-image class
     * 
     * @param string $rendered
     * @param MimeMessage $mimeMessage
     * @return string
     */
    protected function getImagesToEmbedFromSrc($rendered,
            MimeMessage $mimeMessage)
    {
        $chosenImagesNodes = [];
        if ($this->embedImageFromSrc) {
            $dom = new Query($rendered);
            $imageNodeList = $dom->execute('img');
            foreach ($imageNodeList as $imageNode) {
                /* only embed images with src attribute and with class embed-image */
                if ($imageNode->hasAttribute('src') && $imageNode->hasAttribute('class') && $imageNode->getAttribute('class') == 'embed-image') {
                    $chosenImagesNodes[] = $imageNode;
                }
            }
            $rendered = $this->completeEmbedImagesFromSrc($chosenImagesNodes,
                    $imageNodeList, $mimeMessage);
        }
        return $rendered;
    }
    /**
     * Actually embed inline images based on their source
     * 
     * @param array $chosenImagesNodes image nodes in document that need to have their src attribute substituted
     * @param Zend\Dom\NodeList $imageNodeList (all) image nodes in document
     * @param MimeMessage $mimeMessage
     * @return string the html e-mail string
     */
    protected function completeEmbedImagesFromSrc($chosenImagesNodes,
            $imageNodeList, MimeMessage $mimeMessage)
    {
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
        foreach ($chosenImagesNodes as $imageNode) {
            $src = $imageNode->getAttribute('src');
            $content = $this->tryToGetImageFromSrc($src);
            if (false !== $content) {
                $mimePart = $this->createMimePart($content, basename($src),
                        $fileInfo->buffer($content), Mime::DISPOSITION_INLINE); //new ZendMimePart($content);
                $mimeMessage->addPart($mimePart);
                $imageNode->setAttribute('src', 'cid:' . $mimePart->id);
                $this->totalImages++;
            } else {
                $imageNode->parentNode->removeChild($imageNode);
            }
        }
        return $imageNodeList->getDocument()->saveHTML();
    }
    /**
     * Try to get image from file system based on IMG SRC attribute
     * Temporarily turn off warning reporting so that nothing appears in browser
     * when the image is not found.
     * 
     * @param string $src
     * @return boolean|image file contents
     */
    protected function tryToGetImageFromSrc($src)
    {
        /*
         * put application error reporting level in variable so that 
         * it can be set reinstated at end of function
         */
        $errorReportingLevel = error_reporting();
        error_reporting(E_ERROR | E_PARSE);
        $content = file_get_contents($src);
        if (false === $content) {
            $content = file_get_contents(PUBLIC_PATH . $src);
        }
        error_reporting($errorReportingLevel);
        return $content;
    }
    /**
     * Create individual mime part to be attached o the message
     * 
     * @param string $content the data of the file
     * @param string $filename the name of the file
     * @param string $type the mime type of the file
     * @param string $disposition disposition for (part) of header Content-Disposition
     * @return ZendMimePart
     */
    protected function createMimePart($content, $filename, $type, $disposition)
    {
        $mimePart = new ZendMimePart($content);
        $mimePart->id = md5(microtime() . $filename . $this->index++ . microtime());
        $mimePart->type = $type;
        $mimePart->filename = $filename;
        $mimePart->disposition = $disposition;
        $mimePart->encoding = Mime::ENCODING_BASE64;
        return $mimePart;
    }
}