Enabling Multi-part MIME Emails in Magento

This article will explain steps needed for altering Magento transactional emails in order to support Multi-part mime emails. Which means that those emails will support both Text and HTMl versions of email. Core for handling multipart data is already implemented in Zend_Mime module, so there is no need for writing code that will handle necessary headers and boundary for multipart implementation.

So let’s get to it!

Main part that is needed for setting up of multipart emails is located in Mage_Core_Model_Email_Queue->send() function in line 225:

if ($parameters->getIsPlain()) {
                    $mailer->setBodyText($message->getMessageBody());
                } else {
                    $mailer->setBodyHTML($message->getMessageBody());
                }

If we add body text along with body html, Zend_Mime will handle the rest and add needed headers and boundary for multipart support. So the rewrite would go like this:

public function send()
    {
        /** @var $collection Mage_Core_Model_Resource_Email_Queue_Collection */
        $collection = Mage::getModel('core/email_queue')->getCollection()
            ->addOnlyForSendingFilter()
            ->setPageSize(self::MESSAGES_LIMIT_PER_CRON_RUN)
            ->setCurPage(1)
            ->load();
 
        ini_set('SMTP', Mage::getStoreConfig('system/smtp/host'));
        ini_set('smtp_port', Mage::getStoreConfig('system/smtp/port'));
 
        /** @var $message Mage_Core_Model_Email_Queue */
        foreach ($collection as $message) {
            if ($message->getId()) {
                $parameters = new Varien_Object($message->getMessageParameters());
                if ($parameters->getReturnPathEmail() !== null) {
                    $mailTransport = new Zend_Mail_Transport_Sendmail("-f" . $parameters->getReturnPathEmail());
                    Zend_Mail::setDefaultTransport($mailTransport);
                }
 
                $mailer = new Zend_Mail('utf-8');
                foreach ($message->getRecipients() as $recipient) {
                    list($email, $name, $type) = $recipient;
                    switch ($type) {
                        case self::EMAIL_TYPE_BCC:
                            $mailer->addBcc($email, '=?utf-8?B?' . base64_encode($name) . '?=');
                            break;
                        case self::EMAIL_TYPE_TO:
                        case self::EMAIL_TYPE_CC:
                        default:
                            $mailer->addTo($email, '=?utf-8?B?' . base64_encode($name) . '?=');
                            break;
                    }
                }
 
                if ($parameters->getIsPlain()) {
                    $mailer->setBodyText($message->getMessageBody());
                } else {
                    /** INCHOO EDIT START */
                    $mailer->setBodyText($message->getMessageBodyPlain());
                    /** INCHOO EDIT END */
                    $mailer->setBodyHTML($message->getMessageBody());
                }
 
                $mailer->setSubject('=?utf-8?B?' . base64_encode($parameters->getSubject()) . '?=');
                $mailer->setFrom($parameters->getFromEmail(), $parameters->getFromName());
                if ($parameters->getReplyTo() !== null) {
                    $mailer->setReplyTo($parameters->getReplyTo());
                }
                if ($parameters->getReturnTo() !== null) {
                    $mailer->setReturnPath($parameters->getReturnTo());
                }
 
                try {
                    $mailer->send();
                } catch (Exception $e) {
                    Mage::logException($e);
                }
 
                unset($mailer);
                $message->setProcessedAt(Varien_Date::formatDate(true));
                $message->save();
            }
        }
 
        return $this;
    }

Since we don’t have support for message_body_plain attribute added here, we will need to add it to tables and implement the logic for handling this new attribute.

First, lets extend needed classes to add text area for our new attribute when viewing transactional email:
Extend Mage_Adminhtml_Block_System_Email_Template_Edit_Form by overwriting _prepareForm() function to look like this:

protected function _prepareForm()
    {
 
        parent::_prepareForm();
 
        $form = $this->getForm();
 
        $fieldset = $form->getElement('base_fieldset');
 
        $templateId = $this->getEmailTemplate()->getId();
 
        $fieldset->addField('template_text_plain', 'textarea', array(
            'name'=>'template_text_plain',
            'label' => Mage::helper('adminhtml')->__('Template Content - Plain'),
            'title' => Mage::helper('adminhtml')->__('Template Content - Plain'),
            'required' => false,
            'style' => 'height:24em;',
        ));
 
        if ($templateId) {
            $form->addValues($this->getEmailTemplate()->getData());
        }
 
        if ($values = Mage::getSingleton('adminhtml/session')->getData('email_template_form_data', true)) {
            $form->setValues($values);
        }
 
        $this->setForm($form);
 
        return $this;
    }

Now we have added the new field for our plaintext version of email. Next we will add controller support for saving new attribute in database, by extending
Mage_Adminhtml_System_Email_TemplateController class and rewriting saveAction():

public function saveAction()
    {
        $request = $this->getRequest();
        $id = $this->getRequest()->getParam('id');
 
        $template = $this->_initTemplate('id');
        if (!$template->getId() && $id) {
            Mage::getSingleton('adminhtml/session')->addError(
                Mage::helper('adminhtml')->__('This Email template no longer exists.')
            );
            $this->_redirect('*/*/');
            return;
        }
 
        try {
            /** INCHOO EDIT */
            $template->setTemplateSubject($request->getParam('template_subject'))
                ->setTemplateCode($request->getParam('template_code'))
                ->setTemplateText($request->getParam('template_text'))
                ->setTemplateTextPlain($request->getParam('template_text_plain'))
                ->setTemplateStyles($request->getParam('template_styles'))
                ->setModifiedAt(Mage::getSingleton('core/date')->gmtDate())
                ->setOrigTemplateCode($request->getParam('orig_template_code'))
                ->setOrigTemplateVariables($request->getParam('orig_template_variables'));
            /** INCHOO EDIT */
 
            if (!$template->getId()) {
                $template->setAddedAt(Mage::getSingleton('core/date')->gmtDate());
                $template->setTemplateType(Mage_Core_Model_Email_Template::TYPE_HTML);
            }
 
            if ($request->getParam('_change_type_flag')) {
                $template->setTemplateType(Mage_Core_Model_Email_Template::TYPE_TEXT);
                $template->setTemplateStyles('');
            }
 
            $template->save();
            Mage::getSingleton('adminhtml/session')->setFormData(false);
            Mage::getSingleton('adminhtml/session')->addSuccess(
                Mage::helper('adminhtml')->__('The email template has been saved.')
            );
            $this->_redirect('*/*');
        }
        catch (Exception $e) {
            Mage::getSingleton('adminhtml/session')->setData(
                'email_template_form_data',
                $this->getRequest()->getParams()
            );
            Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
            $this->_forward('new');
        }
    }

 After adding controller support for saving the attribute, we need to add additional database columns that will contain plaintext email version along with original HTML/plaintext version of email. We will add template_text_plain column to core_email_template table to contain plaintext skeleton for generating customer templates, and message_body_plain column to core_email_queue table to contain plaintext generated emails. To add necessary columns we can use this script:

/** @var $installer Mage_Core_Model_Resource_Setup*/
$installer = $this;
 
$installer->startSetup();
 
$connection = $installer->getConnection();
 
$emailTemplateTable = Mage::getSingleton('core/resource')->getTableName('core/email_template');
$emailQueueTable = Mage::getSingleton('core/resource')->getTableName('core/email_queue');
 
$connection->addColumn($emailTemplateTable, 'template_text_plain', array(
    'type' => Varien_Db_Ddl_Table::TYPE_TEXT,
    'nullable' => false,
    'comment' => 'Template Text Plain'
));
 
$connection->addColumn($emailQueueTable, 'message_body_plain', array(
    'type' => Varien_Db_Ddl_Table::TYPE_TEXT,
    'nullable' => false,
    'comment' => 'Message Body Plain'
));
 
$installer->endSetup();

Now that we have added our attribute, we need to implement the logic for generating customer emails after placing the order. To do that we will need to extend class Mage_Core_Model_Email_Template and rewrite send() and getProcessedTemplate() functions. In send function we will add call for processing template in plaintext along with normal processing:

public function send($email, $name = null, array $variables = array())
    {
        if (!$this->isValidForSend()) {
            Mage::logException(new Exception('This letter cannot be sent.')); // translation is intentionally omitted
            return false;
        }
 
        $emails = array_values((array)$email);
        $names = is_array($name) ? $name : (array)$name;
        $names = array_values($names);
        foreach ($emails as $key => $email) {
            if (!isset($names[$key])) {
                $names[$key] = substr($email, 0, strpos($email, '@'));
            }
        }
 
        $variables['email'] = reset($emails);
        $variables['name'] = reset($names);
 
        $this->setUseAbsoluteLinks(true);
        /** INCHOO EDIT START */
        $text = $this->getProcessedTemplate($variables);
        $textPlain = $this->getProcessedTemplate($variables, true);
        /** INCHOO EDIT END */
        $subject = $this->getProcessedTemplateSubject($variables);
 
        $setReturnPath = Mage::getStoreConfig(self::XML_PATH_SENDING_SET_RETURN_PATH);
        switch ($setReturnPath) {
            case 1:
                $returnPathEmail = $this->getSenderEmail();
                break;
            case 2:
                $returnPathEmail = Mage::getStoreConfig(self::XML_PATH_SENDING_RETURN_PATH_EMAIL);
                break;
            default:
                $returnPathEmail = null;
                break;
        }
 
        if ($this->hasQueue() && $this->getQueue() instanceof Mage_Core_Model_Email_Queue) {
            /** @var $emailQueue Mage_Core_Model_Email_Queue */
            $emailQueue = $this->getQueue();
            $emailQueue->setMessageBody($text);
            /** INCHOO EDIT START */
            $emailQueue->setMessageBodyPlain($textPlain);
            /** INCHOO EDIT END */
            $emailQueue->setMessageParameters(array(
                'subject'           => $subject,
                'return_path_email' => $returnPathEmail,
                'is_plain'          => $this->isPlain(),
                'from_email'        => $this->getSenderEmail(),
                'from_name'         => $this->getSenderName(),
                'reply_to'          => $this->getMail()->getReplyTo(),
                'return_to'         => $this->getMail()->getReturnPath(),
            ))
                ->addRecipients($emails, $names, Mage_Core_Model_Email_Queue::EMAIL_TYPE_TO)
                ->addRecipients($this->_bccEmails, array(), Mage_Core_Model_Email_Queue::EMAIL_TYPE_BCC);
            $emailQueue->addMessageToQueue();
 
            return true;
        }
 
        ini_set('SMTP', Mage::getStoreConfig('system/smtp/host'));
        ini_set('smtp_port', Mage::getStoreConfig('system/smtp/port'));
 
        $mail = $this->getMail();
 
        if ($returnPathEmail !== null) {
            $mailTransport = new Zend_Mail_Transport_Sendmail("-f".$returnPathEmail);
            Zend_Mail::setDefaultTransport($mailTransport);
        }
 
        foreach ($emails as $key => $email) {
            $mail->addTo($email, '=?utf-8?B?' . base64_encode($names[$key]) . '?=');
        }
 
        if ($this->isPlain()) {
            $mail->setBodyText($text);
        } else {
            $mail->setBodyHTML($text);
        }
 
        $mail->setSubject('=?utf-8?B?' . base64_encode($subject) . '?=');
        $mail->setFrom($this->getSenderEmail(), $this->getSenderName());
 
        try {
            $mail->send();
            $this->_mail = null;
        }
        catch (Exception $e) {
            $this->_mail = null;
            Mage::logException($e);
            return false;
        }
 
        return true;
    }

In getProcessedTemplate function we will alter the way plain and html templates are processed so that when we use html mode, both html and plaintext processing in enabled. Notice we added new $forcePlain argument for controlling how templates are processed:

public function getProcessedTemplate(array $variables = array(), $forcePlain = false)
    {
        $processor = $this->getTemplateFilter();
        /** INCHOO EDIT START */
        if($forcePlain) {
            $processor->setUseSessionInUrl(false)
                ->setPlainTemplateMode(true);
        } else {
            $processor->setUseSessionInUrl(false)
                ->setPlainTemplateMode($this->isPlain());
        }
        /** INCHOO EDIT END */
 
        if (!$this->_preprocessFlag) {
            $variables['this'] = $this;
        }
 
        if (isset($variables['subscriber']) && ($variables['subscriber'] instanceof Mage_Newsletter_Model_Subscriber)) {
            $processor->setStoreId($variables['subscriber']->getStoreId());
        }
 
        // Apply design config so that all subsequent code will run within the context of the correct store
        $this->_applyDesignConfig();
 
        // Populate the variables array with store, store info, logo, etc. variables
        $variables = $this->_addEmailVariables($variables, $processor->getStoreId());
 
        $processor
            ->setTemplateProcessor(array($this, 'getTemplateByConfigPath'))
            ->setIncludeProcessor(array($this, 'getInclude'))
            ->setVariables($variables);
 
        try {
            // Filter the template text so that all HTML content will be present
            /** INCHOO EDIT START */
            if($forcePlain) {
                $result = $processor->filter($this->getTemplateTextPlain());
            } else {
                $result = $processor->filter($this->getTemplateText());
            }
            /** INCHOO EDIT END */
            // If the {{inlinecss file=""}} directive was included in the template, grab filename to use for inlining
            $this->setInlineCssFile($processor->getInlineCssFile());
            // Now that all HTML has been assembled, run email through CSS inlining process
            $processedResult = $this->getPreparedTemplateText($result);
        }
        catch (Exception $e)   {
            $this->_cancelDesignConfig();
            throw $e;
        }
        $this->_cancelDesignConfig();
        return $processedResult;
    }

Lastly we need to add small adjustment to Mage_Adminhtml_Block_System_Email_Template_Preview class in _toHtml() function, which is adjustment for previewing template in admin, and it is needed because of getProcessedTemplate rewrite we did earlier. It is possible to rewrite getProcessedTemplate differently in order to bypass this last rewrite, but I chose this because of easier read:

protected function _toHtml()
    {
        // Start store emulation process
        // Since the Transactional Email preview process has no mechanism for selecting a store view to use for
        // previewing, use the default store view
        $defaultStoreId = Mage::app()->getDefaultStoreView()->getId();
        $appEmulation = Mage::getSingleton('core/app_emulation');
        $initialEnvironmentInfo = $appEmulation->startEnvironmentEmulation($defaultStoreId);
 
        /** @var $template Mage_Core_Model_Email_Template */
        $template = Mage::getModel('core/email_template');
        $id = (int)$this->getRequest()->getParam('id');
        if ($id) {
            $template->load($id);
        } else {
            $template->setTemplateType($this->getRequest()->getParam('type'));
            $template->setTemplateText($this->getRequest()->getParam('text'));
            $template->setTemplateStyles($this->getRequest()->getParam('styles'));
        }
 
        /* @var $filter Mage_Core_Model_Input_Filter_MaliciousCode */
        $filter = Mage::getSingleton('core/input_filter_maliciousCode');
 
        $template->setTemplateText(
            $filter->filter($template->getTemplateText())
        );
 
        Varien_Profiler::start("email_template_proccessing");
        $vars = array();
 
        /** INCHOO EDIT START */
        $templateProcessed = $template->getProcessedTemplate($vars);
        /** INCHOO EDIT END */
 
        if ($template->isPlain()) {
            $templateProcessed = "<pre>" . htmlspecialchars($templateProcessed) . "</pre>";
        }
 
        Varien_Profiler::stop("email_template_proccessing");
 
        // Stop store emulation process
        $appEmulation->stopEnvironmentEmulation($initialEnvironmentInfo);
 
        return $templateProcessed;
    }

That’s it! 😉