<?php
/**
 * File containing the ezcMailFileParser class
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 * @package Mail
 * @version //autogen//
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

/**
 * Parses application/image/video and audio parts.
 *
 * @package Mail
 * @version //autogen//
 * @access private
 */
class ezcMailFileParser extends ezcMailPartParser
{
    /**
     * Default class to handle file attachments when parsing mails
     * is ezcMailFile.
     *
     * Change this to your own file class with:
     * <code>
     * $parser = new ezcMailParser();
     * $parser->options->fileClass = 'myCustomFileClass';
     * // call $parser->parseMail( $set );
     * </code>
     *
     * where myCustomFileClass extends ezcMailFile.
     *
     * @var string
     */
    public static $fileClass = 'ezcMailFile';

    /**
     * Holds the headers for this part.
     *
     * @var ezcMailHeadersHolder
     */
    private $headers = null;

    /**
     * Holds the maintype of the parsed part.
     *
     * @var string
     */
    private $mainType = null;

    /**
     * Holds the subtype of the parsed part.
     *
     * @var string
     */
    private $subType = null;

    /**
     * Holds the filepointer to the attachment.
     *
     * @var resource
     */
    private $fp = null;

    /**
     * Holds the full path and filename of the file to save to.
     *
     * @var string
     */
    private $fileName = null;

    /**
     * Static counter used to generate unique directory names.
     *
     * @var int
     */
    private static $counter = 1;

    /**
     * Holds if data has been written to the output file or not.
     *
     * This is used for delayed filter adding neccessary for quoted-printable.
     *
     * @var bool
     */
    private $dataWritten = false;

    /**
     * Constructs a new ezcMailFileParser with maintype $mainType subtype $subType
     * and headers $headers.
     *
     * @throws ezcBaseFileNotFoundException
     *         if the file attachment file could not be openened.
     * @param string $mainType
     * @param string $subType
     * @param ezcMailHeadersHolder $headers
     */
    public function __construct($mainType, $subType, ezcMailHeadersHolder $headers)
    {
        $this->mainType = $mainType;
        $this->subType = $subType;
        $this->headers = $headers;

        // figure out the base filename
        // search Content-Disposition first as specified by RFC 2183
        $matches = array();
        if (preg_match('/\s*filename="?([^;"]*);?/i',
            $this->headers['Content-Disposition'], $matches)) {
            $fileName = trim($matches[1], '"');
        } // fallback to the name parameter in Content-Type as specified by RFC 2046 4.5.1
        else if (preg_match('/\s*name="?([^;"]*);?/i',
            $this->headers['Content-Type'], $matches)) {
            $fileName = trim($matches[1], '"');
        } else // default
        {
            $fileName = "filename";
        }

        // clean file name (replace unsafe characters with underscores)
        $fileName = strtr($fileName, "/\\\0\"|?*<:;>+[]", '______________');

        $this->fp = $this->openFile($fileName); // propagate exception
    }

    /**
     * Returns the filepointer of the opened file $fileName in a unique directory..
     *
     * This method will create a new unique folder in the temporary directory specified in ezcMailParser.
     * The fileName property of this class will be set to the location of the new file.
     *
     * @throws ezcBaseFileNotFoundException
     *         if the file could not be opened.
     * @param string $fileName
     * @return resource
     */
    private function openFile($fileName)
    {
        // The filename is now relative, we need to extend it with the absolute path.
        // To provide uniqueness we put the file in a directory based on processID and rand.
        $dirName = ezcMailParser::getTmpDir() . getmypid() . '-' . self::$counter++ . '/';
        if (!is_dir($dirName)) {
            mkdir($dirName, 0700);
        }

        // remove the directory and the file when PHP shuts down
        ezcMailParserShutdownHandler::registerForRemoval($dirName);
        $this->fileName = $dirName . $fileName;

        $fp = fopen($this->fileName, 'w');
        if ($this->fp === false) {
            throw new ezcBaseFileNotFoundException($this->fileName);
        }
        return $fp;
    }

    /**
     * Destructs the parser object.
     *
     * Closes and removes any open file.
     */
    public function __destruct()
    {
        // finish() was not called. The mail is completely broken.
        // we will clean up the mess
        if ($this->fp !== null) {
            fclose($this->fp);
            $this->fp = null;
            if ($this->fileName !== null && file_exists($this->fileName)) {
                unlink($this->fileName);
            }
        }
    }

    /**
     * Sets the correct stream filters for the attachment.
     *
     * $line should contain one line of data that should be written to file.
     * It is used to correctly determine the type of linebreak used in the mail.
     *
     * @param string $line
     */
    private function appendStreamFilters($line)
    {
        // append the correct decoding filter
        switch (strtolower($this->headers['Content-Transfer-Encoding'])) {
            case 'base64':
                stream_filter_append($this->fp, 'convert.base64-decode');
                break;
            case 'quoted-printable':
                // fetch the type of linebreak
                preg_match("/(\r\n|\r|\n)$/", $line, $matches);
                $lb = count($matches) > 0 ? $matches[0] : ezcMailTools::lineBreak();

                $param = array('line-break-chars' => $lb);
                stream_filter_append($this->fp, 'convert.quoted-printable-decode',
                    STREAM_FILTER_WRITE, $param);
                break;
            case '7bit':
            case '8bit':
                // do nothing here, file is already just binary
                break;
            default:
                // 7bit default
                break;
        }
    }


    /**
     * Parse the body of a message line by line.
     *
     * This method is called by the parent part on a push basis. When there
     * are no more lines the parent part will call finish() to retrieve the
     * mailPart.
     *
     * The file will be decoded and saved to the given temporary directory within
     * a directory based on the process ID and the time.
     *
     * @param string $line
     */
    public function parseBody($line)
    {
        if ($line !== '') {
            if ($this->dataWritten === false) {
                $this->appendStreamFilters($line);
                $this->dataWritten = true;
            }

            fwrite($this->fp, $line);
        }
    }

    /**
     * Return the result of the parsed file part.
     *
     * This method is called automatically by the parent part.
     *
     * @return ezcMailFile
     */
    public function finish()
    {
        fclose($this->fp);
        $this->fp = null;


        // FIXME: DIRTY PGP HACK
        // When we have PGP support these lines should be removed. They are here now to hide
        // PGP parts since they will show up as file attachments if not.
        if ($this->mainType == "application" &&
            ($this->subType == 'pgp-signature'
                || $this->subType == 'pgp-keys'
                || $this->subType == 'pgp-encrypted')
        ) {
            return null;
        }
        // END DIRTY PGP HACK

        $filePart = new self::$fileClass($this->fileName);

        // set content type
        $filePart->setHeaders($this->headers->getCaseSensitiveArray());
        ezcMailPartParser::parsePartHeaders($this->headers, $filePart);
        switch (strtolower($this->mainType)) {
            case 'image':
                $filePart->contentType = ezcMailFile::CONTENT_TYPE_IMAGE;
                break;
            case 'audio':
                $filePart->contentType = ezcMailFile::CONTENT_TYPE_AUDIO;
                break;
            case 'video':
                $filePart->contentType = ezcMailFile::CONTENT_TYPE_VIDEO;
                break;
            case 'application':
                $filePart->contentType = ezcMailFile::CONTENT_TYPE_APPLICATION;
                break;
        }

        // set mime type
        $filePart->mimeType = $this->subType;

        // set inline disposition mode if set.
        $matches = array();
        if (preg_match('/^\s*inline;?/i',
            $this->headers['Content-Disposition'], $matches)) {
            $filePart->dispositionType = ezcMailFile::DISPLAY_INLINE;
        }
        if (preg_match('/^\s*attachment;?/i',
            $this->headers['Content-Disposition'], $matches)) {
            $filePart->dispositionType = ezcMailFile::DISPLAY_ATTACHMENT;
        }
        $filePart->size = filesize($this->fileName);
        return $filePart;
    }
}

?>
