<?php
/**
 * @license   http://www.horde.org/licenses/gpl GPLv2
 *
 * @copyright 2012-2020 Horde LLC (http://www.horde.org)
 * @author    Michael J Rubinsky <mrubinsk@horde.org>
 * @package   ActiveSync
 */

/**
 * Handle building the AirSyncBaseBody property when sending a full MIME mail
 * structure. I.e., BodyTypePreference == Horde_ActiveSync::BODYPREF_TYPE_MIME.
 *
 * @license   http://www.horde.org/licenses/gpl GPLv2
 *
 * @copyright 2012-2020 Horde LLC (http://www.horde.org)
 * @author    Michael J Rubinsky <mrubinsk@horde.org>
 * @package   ActiveSync
 */
class Horde_ActiveSync_Imap_EasMessageBuilder_Mime extends Horde_ActiveSync_Imap_EasMessageBuilder
{
    /**
     *
     * @param Horde_ActiveSync_Imap_Message $imap_message  The IMAP message object.
     * @param array                         $options       Options array.
     * @param Horde_Log_Logger $logger                     The logger.
     */
    public function __construct(
        Horde_ActiveSync_Imap_Message $imap_message, array $options, $logger)
    {
        parent::__construct($imap_message, $options, $logger);

        $this->_airsyncBody->type = Horde_ActiveSync::BODYPREF_TYPE_MIME;
    }

    /**
     * Perform all tasks.
     */
    protected function _buildBody()
    {
        $this->_buildMessage();
        $this->_doTruncation();
        $this->_easMessage->airsyncbasebody = $this->_airsyncBody;
    }

    /**
     * Determine if we need plain and/or html parts and if we have attachments.
     * Builds appropriate MIME parts and populates $this->_easMessage properties
     * accordingly.
     */
    protected function _buildMessage()
    {
        $this->_logger->meta('Building MIME Message.');
        if (!$this->_canModify()) {
            $this->_buildEncrypted();
            return;
        }

        // The base MIME part.
        $mime = new Horde_Mime_Part();

        // Get a text/plain part if needed.
        if ($this->_mbd->plain) {
            $plain_mime = $this->_buildPlainMime();
        }

        // Get a text/html part if needed.
        if ($this->_mbd->html) {
            $html_mime = $this->_buildHtmlMime();
        }

        // Sanity check the base MIME type
        if (!$this->_mbd->html && !empty($plain_mime)) {
            $mime = $plain_mime;
        } elseif (!$this->_mbd->plain && !empty($html_mime)) {
            $mime = $html_mime;
        } elseif (!empty($plain_mime) && !empty($html_mime)) {
            $mime->setType('multipart/alternative');
            $mime->addPart($plain_mime);
            $mime->addPart($html_mime);
        }

        // If we have attachments, create a multipart/mixed wrapper.
        if ($this->_imapMessage->hasAttachments()) {
            $base = $this->_buildMultipartWrapper($mime);
            $this->_easMessage->airsyncbaseattachments = $this->_imapMessage
                ->getAttachments($this->_version);
        } else {
            $base = $mime;
        }

        // We need to catch Idna exceptions that are leaked up through
        // Horde_Mail and Horde_Mime. However, just ignoring them leads
        // to completely blank emails being generated and sent to the client
        // because they are being thrown while generating the entire Mime
        // structure. Therefore, if we catch an exception, we try again,
        // but this time do not attempt to encode Idna addresses. This
        // could potentially lead to incorrect email addresses being sent
        // to the client, but it is a better alternative than simply removing
        // the email address from the email, thus leaving no record of the
        // recipient/sender/cc etc...
        try {
            $headers = $this->_getHeaders();
            // Populate the EAS body structure with the MIME data.
            $this->_airsyncBody->data = $base->toString(array(
                'headers' => $headers,
                'stream' => true)
            );
        } catch (Horde_Idna_Exception $e) {
            $this->_logger->err($e->getMessage());
            $this->_handleIdnaErrors($headers);
            $this->_airsyncBody->data = $base->toString(array(
                'headers' => $headers,
                'stream' => true)
            );
        }
        $this->_airsyncBody->estimateddatasize = $base->getBytes();
    }

    /**
     * Replace any Horde_Mime-Headers_Addresses objects with
     * our own override, which prevents Idna encoding.
     *
     * @param  Horde_Mime_Headers $headers  The headers object.
     */
    protected function _handleIdnaErrors(Horde_Mime_Headers $headers)
    {
        foreach (array('from', 'to', 'cc') as $name) {
            if ($obj = $headers->getHeader($name)) {
                $obj_idn = new Horde_ActiveSync_Mime_Headers_Addresses($name, $obj->full_value);
                $headers->removeHeader($name);
                $headers->addHeaderOb($obj_idn);
            }
        }
    }

    /**
     * Handle any truncaction and set properties accordingly.
     */
    protected function _doTruncation()
    {
        // @todo Remove this sanity-check hack in 3.0. This is needed
        // since truncationsize incorrectly defaulted to a
        // MIME_TRUNCATION constant and could be cached in the sync-cache.
        $ts = !empty($this->_options['bodyprefs'][Horde_ActiveSync::BODYPREF_TYPE_MIME]['truncationsize'])
            ? $this->_options['bodyprefs'][Horde_ActiveSync::BODYPREF_TYPE_MIME]['truncationsize']
            : false;
        $mime_truncation = (!empty($ts) && $ts > 9)
            ? $ts
            : (!empty($this->_options['truncation']) && $this->_options['truncation'] > 9
                ? $this->_options['truncation']
                : false);

        $this->_logger->meta(sprintf(
            'Checking MIMETRUNCATION: %d, ServerData: %d',
            $mime_truncation,
            $this->_airsyncBody->estimateddatasize)
        );

        if (!empty($mime_truncation) &&
            $this->_airsyncBody->estimateddatasize > $mime_truncation) {
            ftruncate($this->_airsyncBody->data, $mime_truncation);
            $this->_airsyncBody->truncated = '1';
        } else {
            $this->_airsyncBody->truncated = '0';
        }
    }

    /**
     * Returns a Horde_Mime_Part representing a plain text body.
     *
     * @return Horde_Mime_Part
     */
    protected function _buildPlainMime()
    {
        $part = new Horde_Mime_Part();
        $part->setType('text/plain');
        $part->setContents($this->_mbd->plain['body']->stream, array('usestream' => true));
        $part->setCharset('UTF-8');

        return $part;
    }

    /**
     * Returns a Horde_Mime_Part representing a HTML body.
     *
     * @return Horde_Mime_Part
     */
    protected function _buildHtmlMime()
    {
        $part = new Horde_Mime_Part();
        $part->setType('text/html');
        $part->setContents($this->_mbd->html['body']->stream, array('usestream' => true));
        $part->setCharset('UTF-8');

        return $part;
    }

    /**
     * Returns a multipart/mixed Horde_Mime_Part that wraps the body and all
     * attachments parts.
     *
     * @return Horde_Mime_Part
     */
    protected function _buildMultipartWrapper(Horde_Mime_Part $mime)
    {
        $part = new Horde_Mime_Part();
        $part->setType('multipart/mixed');
        $part->addPart($mime);
        $atc = $this->_imapMessage->getAttachmentsMimeParts();
        foreach ($atc as $atc_part) {
            $part->addPart($atc_part);
        }

        return $part;
    }

    /**
     * Returns the headers for the current IMAP message, with the Content-Type
     * and Content-Transfer-Encoding headers removed since we build those
     * ourselves.
     *
     * @return Horde_Mime_Headers
     */
    protected function _getHeaders()
    {
        $headers = $this->_imapMessage->getHeaders();
        $headers->removeHeader('Content-Type');
        $headers->removeHeader('Content-Transfer-Encoding');

        return $headers;
    }

    /**
     * Set airsyncBody properties with the raw IMAP message. Used when message
     * is encrypted and/or signed since we can't modify anything.
     */
    protected function _buildEncrypted()
    {
        $raw = new Horde_ActiveSync_Rfc822(
            $this->_imapMessage->getFullMsg(true),
            false
        );
        $this->_airsyncBody->estimateddatasize = $raw->getBytes();
        $this->_airsyncBody->data = $raw->getString();
        $this->_easMessage->airsyncbaseattachments = $this->_imapMessage->getAttachments($this->_version);
    }

    /**
     * Returns if we are able to (re)build our own MIME message or if we must
     * use the original raw message.
     *
     * @return boolean  True if able to modify false otherwise.
     */
    protected function _canModify()
    {
        return !$this->_imapMessage->isSigned() && !$this->_imapMessage->isEncrypted();
    }

}
