<?php
/**
 * Horde_ActiveSync_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
 * @since 2.19.0
 */
/**
 * This class provides some base functionality for dealing with MIME objects in
 * the context of ActiveSync requests.
 *
 * @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
 * @since 2.19.0
 */
class Horde_ActiveSync_Mime
{
    /**
     * The composited mime part.
     *
     * @var Horde_Mime_Part
     */
    protected $_base;

    /**
     * Local cache of hasAttachments data.
     *
     * @var boolean
     */
    protected $_hasAttachments;

    /**
     * Cont'r
     *
     * @param Horde_Mime_Part $mime The mime data.
     */
    public function __construct(Horde_Mime_Part $mime)
    {
        $this->_base = $mime;
    }

    public function __destruct()
    {
        $this->_base = null;
    }

    /**
     * Accessor
     *
     * @param string $property  The property name.
     *
     * @return mixed
     */
    public function __get($property)
    {
        switch ($property) {
        case 'base':
            return $this->_base;
        }
        return $this->_base->property;
    }

    /**
     * Delegate calls to the composed MIME object.
     *
     * @param string $method  The method name.
     * @param array $params   The parameters.
     *
     * @return mixed
     */
    public function __call($method, array $params)
    {
        if (is_callable(array($this->_base, $method))) {
            return call_user_func_array(array($this->_base, $method), $params);
        }

        throw new InvalidArgumentException();
    }

    /**
     * Return the hasAttachments flag
     *
     * @return boolean
     */
    public function hasAttachments()
    {
        if (isset($this->_hasAttachments)) {
            return $this->_hasAttachments;
        }

        foreach ($this->_base->contentTypeMap() as $id => $type) {
            if ($this->isAttachment($id, $type)) {
                $this->_hasAttachments = true;
                return true;
            }
        }
        $this->_hasAttachments = false;

        return false;
    }

    /**
     * Determines if a MIME type is an attachment.
     * For our purposes, an attachment is any MIME part that can be
     * downloaded by itself (i.e. all the data needed to view the part is
     * contained within the download data).
     *
     * @param string $id         The MIME Id for the part we are checking.
     * @param string $mime_type  The MIME type.
     *
     * @return boolean  True if an attachment.
     * @todo Pass a single mime part as parameter.
     */
    public function isAttachment($id, $mime_type)
    {
        switch ($mime_type) {
        case 'text/plain':
            if (!($this->_base->findBody('plain') == $id)) {
                return true;
            }
            return false;
        case 'text/html':
            if (!($this->_base->findBody('html') == $id)) {
                return true;
            }
            return false;
        case 'application/pkcs7-signature':
        case 'application/x-pkcs7-signature':
            return false;
        }

        if ($this->_base->getPart($id)->getDisposition() == 'attachment') {
            return true;
        }

        list($ptype,) = explode('/', $mime_type, 2);

        switch ($ptype) {
        case 'message':
            return in_array($mime_type, array('message/rfc822', 'message/disposition-notification'));

        case 'multipart':
            return false;

        default:
            return true;
        }
    }

    /**
     * Return the MIME part of the iCalendar attachment, if available.
     *
     * @return mixed  The mime id of an iCalendar part, if present. Otherwise
     *                false.
     */
    public function hasiCalendar()
    {
        if (!$this->hasAttachments()) {
            return false;
        }
        foreach ($this->_base->contentTypeMap() as $id => $type) {
            if ($type == 'text/calendar' || $type == 'application/ics') {
                return $id;
            }
        }

        return false;
    }

    /**
     * Return the S/MIME status of this message (RFC2633)
     *
     * @param Horde_Mime_Part  The part to test. If omitted, uses self::$_base
     *
     * @return boolean  True if message is S/MIME signed, otherwise false.
     */
    public function isSigned(Horde_Mime_Part $mime = null)
    {
        if (empty($mime)) {
            $mime = $this->_base;
        }

        if ($mime->getPrimaryType() == 'multipart') {
            if ($mime->getSubType() == 'signed' && $mime->getContentTypeParameter('protocol') != 'application/pgp-signature') {
                return true;
            }

            // Signed/encrypted part might be lower in the mime structure
            foreach ($mime->getParts() as $part) {
                if ($this->isSigned($part)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Return the S/MIME encryption status of this message.
     *
     * @param Horde_Mime_Part  The part to test. If omitted, uses self::$_base
     *
     * @return boolean  True if message is S/MIME encrypted, otherwise false.
     * @since 2.20.0
     * @todo For 3.0, combine into one method with self::isSigned() and return
     *       a bitmask result.
     */
    public function isEncrypted(Horde_Mime_Part $mime = null)
    {
        if (empty($mime)) {
            $mime = $this->_base;
        }

        if ($mime->getType() == 'application/pkcs7-mime' ||
            $mime->getType() == 'application/x-pkcs7-mime') {
            return true;
        }

        // Signed/encrypted part might be lower in the mime structure
        foreach ($mime->getParts() as $part) {
            if ($this->isEncrypted($part)) {
                return true;
            }
        }
    }

    /**
     * Finds the main "body" text part (if any) in a message. "Body" data is the
     * first text part under this part. Considers only body data that should
     * be displayed as the main body on an EAS client. I.e., this ignores any
     * text parts contained withing "attachment" parts such as messages/rfc822
     * attachments.
     *
     * @param string $subtype  Specifically search for this subtype.
     *
     * @return mixed  The MIME ID of the main body part, or null if a body
     *                part is not found.
     */
    public function findBody($subtype = null)
    {
        $this->buildMimeIds();
        $iterator = new Horde_ActiveSync_Mime_Iterator($this->_base, true);
        foreach ($iterator as $val) {
            $id = $val->getMimeId();
            if (($val->getPrimaryType() == 'text') &&
                ((intval($id) === 1) || !$this->getMimeId()) &&
                (is_null($subtype) || ($val->getSubType() == $subtype)) &&
                !$this->isAttachment($id, $val->getType())) {
                return $id;
            }
        }

        return null;
    }

    /**
     * Replace the specified MimePart with the provided part.
     *
     * @param string $id               The part id of the part to replace.
     * @param Horde_Mime_Part $newPart The new mimePart.
     */
    public function alterPart($id, $newPart)
    {
        $this->_base[$id] = $newPart;
    }

}
