<?php
/**
 * File containing the ezcMail 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
 */

/**
 * The main mail class.
 *
 * You can use ezcMail together with the other classes derived from ezcMailPart
 * to build email messages. When the mail is built, use the Transport classes
 * to send the mail.
 *
 * This example builds and sends a simple text mail message:
 * <code>
 * $mail = new ezcMail;
 * $mail->from = new ezcMailAddress( 'sender@example.com', 'Adrian Ripburger' );
 * $mail->addTo( new ezcMailAddress( 'receiver@example.com', 'Maureen Corley' ) );
 * $mail->subject = "Hi";
 * $mail->body = new ezcMailText( "I just mail to say I love you!" );
 * $transport = new ezcMailMtaTransport();
 * $transport->send( $mail );
 * </code>
 *
 * By default, the ezcMail class will generate a mail with the Bcc header inside,
 * and leave it to the SMTP server to strip the Bcc header. This can pose a
 * problem with some SMTP servers which do not strip the Bcc header
 * (issue #16154: Bcc headers are not stripped when using SMTP). Use the option
 * stripBccHeader from {@link ezcMailOptions} to delete the Bcc header from
 * the mail before it is sent.
 *
 * Example:
 * <code>
 * $options = new ezcMailOptions();
 * $options->stripBccHeader = true; // default value is false
 *
 * $mail = new ezcMail( $options );
 *
 * You can also derive your own mail classes from this class if you have
 * special requirements. An example of this is the ezcMailComposer class which
 * is a convenience class to send simple mail structures and HTML mail.
 *
 * There are several headers you can set on the mail object to achieve various
 * effects:
 * - Reply-To - Set this to an email address if you want people to reply to an
 *              address other than the from address.
 * - Errors-To - If the mail can not be delivered the error message will be
 *               sent to this address.
 *
 * @property ezcMailAddress        $from Contains the from address as an
 *                                       ezcMailAddress object.
 * @property array(ezcMailAddress) $to   Contains an array of ezcMailAddress objects.
 * @property array(ezcMailAddress) $cc   Contains an array of ezcMailAddress objects.
 * @property array(ezcMailAddress) $bcc  Contains an array of ezcMailAddress objects.
 * @property string                $subject
 *                                       Contains the subject of the e-mail.
 *                                       Use setSubject if you require a
 *                                       special encoding.
 * @property string                $subjectCharset
 *                                       The encoding of the subject.
 * @property ezcMailPart           $body The body part of the message.
 *
 * @property-read string           $messageId
 *                                       The message ID of the message. Treat
 *                                       as read-only unless you're 100% sure
 *                                       what you're doing. Also accessible through
 *                                       the deprecated property messageID.
 * @property-read integer          $timestamp
 *                                       The date/time of when the message was
 *                                       sent as Unix Timestamp.
 * @property ezcMailAddress        $returnPath Contains the Return-Path address as an
 *                                             ezcMailAddress object. The email
 *                                             address embedded in the
 *                                             ezcMailAddress object may only
 *                                             contain letters from the
 *                                             RETURN_PATH_CHARS set.
 * @property ezcMailOptions $options
 *           Options for generating mail. See {@link ezcMailOptions}.
 *
 * @apichange Remove the support for the deprecated property messageID.
 *
 * @package Mail
 * @version //autogen//
 * @mainclass
 */
class ezcMail extends ezcMailPart
{
    /**
     * 7 bit encoding.
     */
    const SEVEN_BIT = "7bit";

    /**
     * 8 bit encoding.
     */
    const EIGHT_BIT = "8bit";

    /**
     * Binary encoding.
     */
    const BINARY = "binary";

    /**
     * Quoted printable encoding.
     */
    const QUOTED_PRINTABLE = "quoted-printable";

    /**
     * Base 64 encoding.
     */
    const BASE64 = "base64";

    /**
     * Characters allowed in the returnPath address
     */
    const RETURN_PATH_CHARS = 'A-Za-z0-9_.@=/+{}#~\-\'';

    /**
     * Holds the options for this class.
     *
     * @var ezcMailOptions
     */
    protected $options;

    /**
     * Constructs an empty ezcMail object.
     */
    public function __construct( ezcMailOptions $options = null )
    {
        parent::__construct();

        $this->properties['from'] = null;
        $this->properties['to'] = array();
        $this->properties['cc'] = array();
        $this->properties['bcc'] = array();
        $this->properties['subject'] = null;
        $this->properties['subjectCharset'] = 'us-ascii';
        $this->properties['body'] = null;
        $this->properties['messageId'] = null;
        $this->properties['returnPath'] = null;

        if ( $options === null )
        {
            $options = new ezcMailOptions();
        }

        $this->options = $options;
    }

    /**
     * Sets the property $name to $value.
     *
     * @throws ezcBasePropertyNotFoundException
     *         if the property does not exist.
     * @throws ezcBasePropertyPermissionException
     *         if the property is read-only.
     * @param string $name
     * @param mixed $value
     * @ignore
     */
    public function __set( $name, $value )
    {
        switch ( $name )
        {
            case 'returnPath':
                if ( $value !== null && !$value instanceof ezcMailAddress )
                {
                    throw new ezcBaseValueException( $name, $value, 'ezcMailAddress or null' );
                }
                if ( $value !== null && preg_replace( '([' . self::RETURN_PATH_CHARS . '])', '', $value->email ) != '' )
                {
                    throw new ezcBaseValueException( $name, $value->email, 'the characters \'' . self::RETURN_PATH_CHARS . '\'' );
                }
                $this->properties[$name] = $value;
                break;

            case 'from':
                if ( $value !== null && !$value instanceof ezcMailAddress )
                {
                    throw new ezcBaseValueException( $name, $value, 'ezcMailAddress or null' );
                }
                $this->properties[$name] = $value;
                break;

            case 'to':
            case 'cc':
            case 'bcc':
                if ( !is_array( $value ) )
                {
                    throw new ezcBaseValueException( $name, $value, 'array( ezcMailAddress )' );
                }
                foreach ( $value as $key => $obj )
                {
                    if ( !$obj instanceof ezcMailAddress )
                    {
                        throw new ezcBaseValueException( "{$name}[{$key}]", $obj, 'ezcMailAddress' );
                    }
                }
                $this->properties[$name] = $value;
                break;

            case 'subject':
                $this->properties['subject'] = trim( $value );
                break;

            case 'subjectCharset':
                $this->properties['subjectCharset'] = $value;
                break;

            case 'body':
                if ( !$value instanceof ezcMailPart )
                {
                    throw new ezcBaseValueException( $name, $value, 'ezcMailPart' );
                }
                $this->properties['body'] = $value;
                break;

            case 'messageId':
            case 'messageID':
                $this->properties['messageId'] = $value;
                break;

            case 'timestamp':
                throw new ezcBasePropertyPermissionException( $name, ezcBasePropertyPermissionException::READ );
                break;

            default:
                parent::__set( $name, $value );
                break;
        }
    }

    /**
     * Returns the property $name.
     *
     * @throws ezcBasePropertyNotFoundException
     *         if the property does not exist.
     * @param string $name
     * @return mixed
     * @ignore
     */
    public function __get( $name )
    {
        switch ( $name )
        {
            case 'to':
            case 'cc':
            case 'bcc':
                return (array) $this->properties[$name];

            case 'from':
            case 'subject':
            case 'subjectCharset':
            case 'body':
            case 'messageId':
            case 'returnPath':
                return $this->properties[$name];

            case 'messageID': // deprecated version
                return $this->properties['messageId'];

            case 'timestamp':
                return strtotime( $this->getHeader( "Date" ) );

            default:
                return parent::__get( $name );
        }
    }

    /**
     * Returns true if the property $name is set, otherwise false.
     *
     * @param string $name
     * @return bool
     * @ignore
     */
    public function __isset( $name )
    {
        switch ( $name )
        {
            case 'to':
            case 'cc':
            case 'bcc':
            case 'from':
            case 'subject':
            case 'subjectCharset':
            case 'body':
            case 'messageId':
            case 'returnPath':
                return isset( $this->properties[$name] );

            case 'messageID': // deprecated version
                return isset( $this->properties['messageId'] );

            case 'timestamp':
                return $this->getHeader( "Date" ) != null;

            default:
                return parent::__isset( $name );
        }
    }

    /**
     * Adds the ezcMailAddress $address to the list of 'to' recipients.
     *
     * @param ezcMailAddress $address
     */
    public function addTo( ezcMailAddress $address )
    {
        $this->properties['to'][] = $address;
    }

    /**
     * Adds the ezcMailAddress $address to the list of 'cc' recipients.
     *
     * @param ezcMailAddress $address
     */
    public function addCc( ezcMailAddress $address )
    {
        $this->properties['cc'][] = $address;
    }

    /**
     * Adds the ezcMailAddress $address to the list of 'bcc' recipients.
     *
     * @param ezcMailAddress $address
     */
    public function addBcc( ezcMailAddress $address )
    {
        $this->properties['bcc'][] = $address;
    }

    /**
     * Returns the generated body part of this mail.
     *
     * Returns an empty string if no body has been set.
     *
     * @return string
     */
    public function generateBody()
    {
        if ( is_subclass_of( $this->body, 'ezcMailPart' ) )
        {
            return $this->body->generateBody();
        }
        return '';
    }

    /**
     * Returns the generated headers for the mail.
     *
     * This method is called automatically when the mail message is built.
     * You can re-implement this method in subclasses if you wish to set
     * different mail headers than ezcMail.
     *
     * @return string
     */
    public function generateHeaders()
    {
        // set our headers first.
        if ( $this->from !== null )
        {
            $this->setHeader( "From", ezcMailTools::composeEmailAddress( $this->from ) );
        }

        if ( $this->to !== null )
        {
            $this->setHeader( "To", ezcMailTools::composeEmailAddresses( $this->to ) );
        }
        if ( count( $this->cc ) )
        {
            $this->setHeader( "Cc", ezcMailTools::composeEmailAddresses( $this->cc ) );
        }
        if ( count( $this->bcc ) && $this->options->stripBccHeader === false )
        {
            $this->setHeader( "Bcc", ezcMailTools::composeEmailAddresses( $this->bcc ) );
        }

        $this->setHeader( 'Subject', $this->subject, $this->subjectCharset );

        $this->setHeader( 'MIME-Version', '1.0' );
        $this->setHeader( 'User-Agent', 'Apache Zeta Components' );
        $this->setHeader( 'Date', date( 'r' ) );
        $idhost = $this->from != null && $this->from->email != '' ? $this->from->email : 'localhost';
        if ( is_null( $this->messageId ) )
        {
            $this->setHeader( 'Message-Id', '<' . ezcMailTools::generateMessageId( $idhost ) . '>' );
        }
        else
        {
            $this->setHeader( 'Message-Id', $this->messageID );
        }

        // if we have a body part, include the headers of the body
        if ( is_subclass_of( $this->body, "ezcMailPart" ) )
        {
            return parent::generateHeaders() . $this->body->generateHeaders();
        }
        return parent::generateHeaders();
    }

    /**
     * Returns an array of mail parts from the current mail.
     *
     * The array returned contains objects of classes:
     * - ezcMailText
     * - ezcMailFile
     * - ezcMailRfc822Digest
     * If the method is called with $includeDigests as true, then the returned
     * array will not contain ezcMailRfc822Digest objects, but instead the mail
     * parts inside the digests.
     * The parameter $filter can be used to restrict the returned mail parts,
     * eg. $filter = array( 'ezcMailFile' ) to return only file mail parts.
     *
     * A typical use for this function is to get a list of attachments from a mail.
     * Example:
     * <code>
     * // $mail is an ezcMail object
     * $parts = $mail->fetchParts();
     * // after the above line is executed, $parts will contain an array of mail parts objects,
     * // for example one ezcMailText object ($parts[0]) and two ezcMailRfc822Digest objects ($parts[1] and $parts[2]).
     * // the ezcMailText object will be used to render the mail text, and the
     * // other two objects will be displayed as links ("view attachment")
     *
     * // when user clicks on one of the two attachments, the parts of that attachment
     * // must be retrieved in order to render the attached digest:
     * $subparts = $parts[1]->mail->fetchParts();
     * // after the above line is executed, $subparts will contain an array of mail parts objects,
     * // for example one ezcMailText object and one ezcMailFile object
     * </code>
     *
     * @param array(string) $filter
     * @param bool $includeDigests
     * @return array(ezcMailPart)
     */
    public function fetchParts( $filter = null, $includeDigests = false )
    {
        $context = new ezcMailPartWalkContext( array( __CLASS__, 'collectPart' ) );
        $context->includeDigests = $includeDigests;
        $context->filter = $filter;
        $context->level = 0;
        $this->walkParts( $context, $this );
        return $context->getParts();
    }

    /**
     * Walks recursively through the mail parts in the specified mail object.
     *
     * $context is an object of class ezcMailPartWalkContext, which must contain
     * a valid callback function name to be applied to all mail parts. You can use
     * the collectPart() method, or create your own callback function which can
     * for example save the mail parts to disk or to a database.
     *
     * For the properties you can set to the walk context see: {@link ezcMailPartWalkContext}
     *
     * Example:
     * <code>
     * class App
     * {
     *     public static function saveMailPart( $context, $mailPart )
     *     {
     *         // code to save the $mailPart object to disk
     *     }
     * }
     *
     * // use the saveMailPart() function as a callback in walkParts()
     * // where $mail is an ezcMail object.
     * $context = new ezcMailPartWalkContext( array( 'App', 'saveMailPart' ) );
     * $context->includeDigests = true; // if you want to go through the digests in the mail
     * $mail->walkParts( $context, $mail );
     * </code>
     *
     * @param ezcMailPartWalkContext $context
     * @param ezcMailPart $mail
     */
    public function walkParts( ezcMailPartWalkContext $context, ezcMailPart $mail )
    {
        $className = get_class( $mail );
        $context->level++;
        switch ( $className )
        {
            case 'ezcMail':
            case 'ezcMailComposer':
                if ( $mail->body !== null )
                {
                    $this->walkParts( $context, $mail->body );
                }
                break;

            case 'ezcMailMultipartMixed':
            case 'ezcMailMultipartAlternative':
            case 'ezcMailMultipartDigest':
            case 'ezcMailMultipartReport':
                foreach ( $mail->getParts() as $part )
                {
                    $this->walkParts( $context, $part );
                }
                break;

            case 'ezcMailMultipartRelated':
                $this->walkParts( $context, $mail->getMainPart() );
                foreach ( $mail->getRelatedParts() as $part )
                {
                    $this->walkParts( $context, $part );
                }
                break;

            case 'ezcMailRfc822Digest':
                if ( $context->includeDigests )
                {
                    $this->walkParts( $context, $mail->mail );
                }
                elseif ( empty( $context->filter ) || in_array( $className, $context->filter ) )
                {
                    call_user_func( $context->callbackFunction, $context, $mail );
                }
                break;

            case 'ezcMailText':
            case 'ezcMailFile':
            case 'ezcMailVirtualFile':
            case 'ezcMailStreamFile':
            case 'ezcMailDeliveryStatus':
                if ( empty( $context->filter ) || in_array( $className, $context->filter ) )
                {
                    call_user_func( $context->callbackFunction, $context, $mail );
                }
                break;

            default:
                // for cases where a custom mail class has been specified with $parser->options->mailClass
                if ( in_array( 'ezcMail', class_parents( $className ) ) )
                {
                    if ( $mail->body !== null )
                    {
                        $this->walkParts( $context, $mail->body );
                    }
                }

                // for cases where a custom file class has been specified with $parser->options->fileClass
                if ( in_array( 'ezcMailFile', class_parents( $className ) ) )
                {
                    if ( empty( $context->filter ) || in_array( $className, $context->filter ) )
                    {
                        call_user_func( $context->callbackFunction, $context, $mail );
                    }
                }
        }
        $context->level--;
    }

    /**
     * Saves $mail in the $context object.
     *
     * This function is used as a callback in the fetchParts() method.
     *
     * @param ezcMailPartWalkContext $context
     * @param ezcMailPart $mail
     */
    protected static function collectPart( ezcMailPartWalkContext $context, ezcMailPart $mail )
    {
        $context->appendPart( $mail );
    }
}
?>
