<?php
/**
 * Horde_ActiveSync_Rfc822::
 *
 * @license   http://www.horde.org/licenses/gpl GPLv2
 *
 * @copyright 2010-2020 Horde LLC (http://www.horde.org)
 * @author    Michael J Rubinsky <mrubinsk@horde.org>
 * @package   ActiveSync
 */
 /**
  * Horde_ActiveSync_Rfc822:: class provides functionality related to dealing
  * with raw RFC822 message strings within an ActiveSync context.
  *
  * @license   http://www.horde.org/licenses/gpl GPLv2
  * @copyright 2010-2020 Horde LLC (http://www.horde.org)
  * @author    Michael J Rubinsky <mrubinsk@horde.org>
  * @package   ActiveSync
  */
class Horde_ActiveSync_Rfc822
{
    /**
     * The memory limit for use with the PHP temp stream.
     *
     * @var integer
     */
    public static $memoryLimit = 2097152;

    /**
     * Position of end of headers.
     *
     * @var integer
     */
    protected $_hdr_pos;

    /**
     * The size of the EOL sequence.
     *
     * @var integer
     */
    protected $_eol;

    /**
     * The raw message data in a stream.
     *
     * @var Horde_Stream
     */
    protected $_stream;

    /**
     * The header text.
     *
     * @var string
     */
    protected $_header_text;

    /**
     * Constructor.
     *
     * @param mixed $rfc822              The incoming message. Either a string
     *                                   or a stream resource.
     * @param boolean $auto_add_headers  Automatically add the standard
     *                                   Message-ID and User-Agent headers?
     *                                   @since 2.14.0
     */
    public function __construct($rfc822, $auto_add_headers = true)
    {
        if (is_resource($rfc822)) {
            $stream = new Horde_Stream_Existing(array('stream' => $rfc822));
            $stream->rewind();
        } else {
            $stream = new Horde_Stream_Temp(array('max_memory' => self::$memoryLimit));
            $stream->add($rfc822, true);
        }
        $this->_parseStream($stream);
        if ($auto_add_headers) {
            $this->addStandardHeaders();
        }
    }

    /**
     * Parse a Horde_Stream object to get the header and eol data.
     *
     * @param  Horde_Stream $stream  The stream object.
     */
    protected function _parseStream(Horde_Stream $stream)
    {
        $this->_stream = $stream;
        list($this->_hdr_pos, $this->_eol) = $this->_findHeader();
    }

    /**
     * Returns the raw message with the message headers stripped.
     *
     * @return Horde_Stream
     */
    public function getMessage()
    {
        // Position to after the headers.
        fseek($this->_stream->stream, $this->_hdr_pos + $this->_eol);
        $new_stream = new Horde_Stream_Temp(array('max_memory' => self::$memoryLimit));
        $new_stream->add($this->_stream, true);
        return $new_stream;
    }

    /**
     * Replace the MIME part of the message sent from the client. Headers from
     * the original message are always used.
     *
     * @param  Horde_Mime_Part $part  The new MIME part.
     * @since 2.19.0
     */
    public function replaceMime(Horde_Mime_Part $part)
    {
        $mime_stream = $part->toString(array(
            'stream' => true,
            'headers' => false)
        );
        $mime_stream = new Horde_Stream_Existing(array('stream' => $mime_stream));

        // Since we are still using the headers sent from the device, we can
        // simply zero out the position members etc...
        $this->_hdr_pos = 0;
        $this->_stream = $mime_stream;
        $mime_stream->rewind();
    }

    /**
     * Return the raw message data.
     *
     * @return stream resource
     * @todo Rename to make it clear this returns a stream.
     */
    public function getString()
    {
        if (!empty($this->_header_text)) {
            return Horde_Stream_Wrapper_Combine::getStream(array($this->_header_text, $this->getMessage()->stream));
        } else {
            $this->_stream->rewind();
            return $this->_stream->stream;
        }
    }

    /**
     * Return the message headers.
     *
     * @return Horde_Mime_Headers  The header object.
     */
    public function getHeaders()
    {
        if (!empty($this->_header_text)) {
            $hdr_text = $this->_header_text;
        } else {
            $this->_stream->rewind();
            $hdr_text = $this->_stream->substring(0, $this->_hdr_pos);
        }

        return Horde_Mime_Headers::parseHeaders($hdr_text);
    }

    /**
     * Check for and add standard headers if needed.
     *
     * @since 2.14.0
     */
    public function addStandardHeaders()
    {
        $headers = $this->getHeaders();
        $updated = false;
        // Check for required headers.
        if (!$headers->getValue('Message-ID')) {
            $headers->addMessageIdHeader();
            $updated = true;
        }
        if (!$headers->getValue('User-Agent')) {
            $headers->addUserAgentHeader();
            $updated = true;
        }
        if (!$headers->getValue('Date')) {
            $d = new Horde_Date();
            $headers->addHeaderOb(Horde_Mime_Headers_Date::create());
            $updated = true;
        }
        if ($updated) {
            $this->_header_text = $headers->toString(array('charset' => 'UTF-8'));
        }
    }

    /**
     * Return a Mime object representing the entire message.
     *
     * @return Horde_Mime_Part  The Mime object.
     */
    public function getMimeObject()
    {
        $this->_stream->rewind();
        $part = Horde_Mime_Part::parseMessage($this->_stream->getString());
        $part->isBasePart(true);

        return $part;
    }

    /**
     * Return the length of the message data.
     *
     * @return integer
     */
    public function getBytes()
    {
        if (!isset($this->_bytes)) {
            $this->_bytes = $this->_stream->length();
        }

        return $this->_bytes;
    }

    /**
     * Find the location of the end of the header text.
     *
     * @return array  1st element: Header position, 2nd element: Length of
     *                trailing EOL.
     */
    protected function _findHeader()
    {
        // Look for the EOL that is found first in the message. Some clients
        // are sending mixed EOL in S/MIME signed messages. This still doesn't
        // fix "Nine" currently, as they first send \r\n, but use \n\n to
        // separate the headers.
        // See: https://ninefolders.plan.io/track/10606/1dcfed
        switch ($this->_stream->getEOL()) {
        case "\n":
            return array($this->_stream->search("\n\n"), 2);
        case "\r\n":
            return array($this->_stream->search("\r\n\r\n"), 4);
        }
    }

}
