Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Mail/Message.php
<?php
/**
 * 
 * Class to build an email message for sending through a transport.
 * 
 * Heavily modified and refactored from Zend_Mail_Message and related classes.
 * 
 * @category Solar
 * 
 * @package Solar_Mail Email-sending tools.
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: Message.php 4555 2010-05-05 21:44:55Z pmjones $
 * 
 */
class Solar_Mail_Message extends Solar_Base
{
    /**
     * 
     * Default configuration values.
     * 
     * @config string boundary The default boundary value for separating message parts.
     * 
     * @config string charset The character-set for messages; default is 'utf-8'.
     * 
     * @config string encoding The encoding for messages; default is '8bit'.
     * 
     * @config string crlf The line-ending string to use; default is "\r\n".
     * 
     * @config array headers An array of key-value pairs where the key is the header label
     *   and the value is the header value.  Default null.
     * 
     * @config dependency transport A Solar_Mail_Transport dependency injection, for use 
     *   with the send() method.  Default null, which means you need to send
     *   this message through a separate transport object.
     * 
     * @var array
     * 
     */
    protected $_Solar_Mail_Message = array(
        'boundary'    => null,
        'charset'     => 'utf-8',
        'encoding'    => '8bit',
        'crlf'        => "\r\n",
        'headers'     => null,
        'transport'   => null,
    );
    
    /**
     * 
     * Array of MIME part attachments for this message.
     * 
     * @var array
     * 
     */
    protected $_atch = array();
    
    /**
     * 
     * The MIME boundary string to separate the parts in this message.
     * 
     * @var string
     * 
     */
    protected $_boundary = null;
    
    /**
     * 
     * Encoding used for this message.
     * 
     * @var string
     * 
     */
    protected $_encoding = '8bit';
    
    /**
     * 
     * Character set used for this message.
     * 
     * @var string
     * 
     */
    protected $_charset = 'utf-8';
    
    /**
     * 
     * The line ending to use for this message.
     * 
     * @var string
     * 
     */
    protected $_crlf = "\r\n";
    
    /**
     * 
     * The "From:" address and display-name.
     * 
     * @var array
     * 
     */
    protected $_from = array('', '');
    
    /**
     * 
     * Array of custom additional headers.
     * 
     * @var array
     * 
     */
    protected $_headers = array();
    
    /**
     * 
     * The Solar_Mail_Message_Part for the "text/html" portion of the message.
     * 
     * @var Solar_Mail_Message_Part
     * 
     */
    protected $_html = null;
    
    /**
     * 
     * All recipient address and display-name values.
     * 
     * @var array
     * 
     */
    protected $_rcpt = array(
        'To'  => array(),
        'Cc'  => array(),
        'Bcc' => array(),
    );
    
    /**
     * 
     * The "Reply-To:" address and display-name.
     * 
     * @var array
     * 
     */
    protected $_reply_to = array('', '');
    
    /**
     * 
     * The "Return-Path" value.
     * 
     * @var string 
     * 
     */
    protected $_return_path = null;
    
    /**
     * 
     * The "Subject" value.
     * 
     * @var string
     * 
     */
    protected $_subject = null;
    
    /**
     * 
     * The Solar_Mail_Message_Part for the "text/plain" portion of the message.
     * 
     * @var Solar_Mail_Message_Part
     * 
     */
    protected $_text = null;
    
    /**
     * 
     * A Solar_Mail_Transport dependency object.
     * 
     * @var Solar_Mail_Transport_Adapter
     * 
     */
    protected $_transport = null;
    
    /**
     * 
     * Post-construction tasks to complete object construction.
     * 
     * @return void
     * 
     */
    protected function _postConstruct()
    {
        parent::_postConstruct();
        
        // custom boundary string
        if ($this->_config['boundary']) {
            $this->_boundary = $this->_config['boundary'];
        } else {
            $this->_boundary = '__' . hash('md5', uniqid());
        }
        
        // custom encoding
        if ($this->_config['encoding']) {
            $this->_encoding = $this->_config['encoding'];
        }
        
        // custom charset
        if ($this->_config['charset']) {
            $this->_charset = $this->_config['charset'];
        }
        
        // custom CRLF
        if ($this->_config['crlf']) {
            $this->_crlf = $this->_config['crlf'];
        }
        
        // custom headers
        if ($this->_config['headers']) {
            foreach ((array) $this->_config['headers'] as $label => $value) {
                $this->addHeader($label, $value);
            }
        }
        
        // do we have an injected transport?
        if ($this->_config['transport']) {
            $this->_transport = Solar::dependency(
                'Solar_Mail_Transport',
                $this->_config['transport']
            );
        }
    }
    
    /**
     * 
     * Sets the Solar_Mail_Transport dependency.
     * 
     * @param Solar_Mail_Transport_Adapter $transport The transport adapter.
     * 
     * @return Solar_Mail_Message
     * 
     */
    public function setTransport($transport)
    {
        $this->_transport = $transport;
        return $this;
    }
    
    /**
     * 
     * Sets the CRLF sequence for this message.
     * 
     * @param string $crlf The CRLF line-ending string.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setCrlf($crlf)
    {
        $this->_crlf = $crlf;
        return $this;
    }
    
    /**
     * 
     * Returns the CRLF sequence for this message.
     * 
     * @return string
     * 
     */
    public function getCrlf()
    {
        return $this->_crlf;
    }
    
    /**
     * 
     * Sets the encoding for this message.
     * 
     * @param string $encoding The encoding.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setEncoding($encoding)
    {
        $this->_encoding = $encoding;
        return $this;
    }
    
    /**
     * 
     * Returns the encoding for this message.
     * 
     * @return string
     * 
     */
    public function getEncoding()
    {
        return $this->_encoding;
    }
    
    /**
     * 
     * Sets the character set for this message.
     * 
     * @param string $charset The character set.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setCharset($charset)
    {
        $this->_charset = $charset;
        return $this;
    }
    
    /**
     * 
     * Returns the character set for this message.
     * 
     * @return string
     * 
     */
    public function getCharset()
    {
        return $this->_charset;
    }
    
    /**
     * 
     * Sets the Return-Path header for an email.
     * 
     * @param string $addr The email address of the return-path.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setReturnPath($addr)
    {
        $this->_return_path = $addr;
        return $this;
    }
    
    /**
     * 
     * Returns the current Return-Path address for the email.
     * 
     * @return string
     * 
     */
    public function getReturnPath()
    {
        return $this->_return_path;
    }
    
    /**
     * 
     * Sets the "From:" (sender) on this message.
     * 
     * Strips CR/LF from the address and name to help avoid header injections.
     * 
     * @param string $addr The email address of the sender.
     * 
     * @param string $name The display-name for the sender, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setFrom($addr, $name = '')
    {
        $this->_from = array(
            $addr,
            $name,
        );
        
        return $this;
    }
    
    /**
     * 
     * Returns the "From:" address for this message.
     * 
     * @return array
     * 
     */
    public function getFrom()
    {
        return $this->_from;
    }
    
    /**
     * 
     * Sets the "Reply-To:" on this message.
     * 
     * Strips CR/LF from the address and name to help avoid header injections.
     * 
     * @param string $addr The email address of the sender.
     * 
     * @param string $name The display-name for the sender, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setReplyTo($addr, $name = '')
    {
        $this->_reply_to = array(
            $addr,
            $name,
        );
        
        return $this;
    }
    
    /**
     * 
     * Returns the "Reply-To:" address for this message.
     * 
     * @return array
     * 
     */
    public function getReplyTo()
    {
        return $this->_reply_to;
    }
    
    /**
     * 
     * Adds a "To:" address recipient.
     * 
     * @param string $addr The email address of the recipient.
     * 
     * @param string $name The display-name for the recipient, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function addTo($addr, $name = null)
    {
        $this->_rcpt['To'][$addr] = $name;
        return $this;
    }
    
    /**
     * 
     * Sets the "To:" address recipient, removing previous "To:" recipients.
     * 
     * @param string $addr The email address of the recipient.
     * 
     * @param string $name The display-name for the recipient, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setTo($addr, $name = null)
    {
        $this->_rcpt['To'] = array($addr => $name);
        return $this;
    }
    
    /**
     * 
     * Adds a "Cc:" address recipient.
     * 
     * @param string $addr The email address of the recipient.
     * 
     * @param string $name The display-name for the recipient, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function addCc($addr, $name = null)
    {
        $this->_rcpt['Cc'][$addr] = $name;
        return $this;
    }
    
    /**
     * 
     * Sets the "Cc:" address recipient, removing previous "Cc:" recipients.
     * 
     * @param string $addr The email address of the recipient.
     * 
     * @param string $name The display-name for the recipient, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setCc($addr, $name = null)
    {
        $this->_rcpt['Cc'] = array($addr => $name);
        return $this;
    }
    
    /**
     * 
     * Adds a "Bcc:" address recipient.
     * 
     * @param string $addr The email address of the recipient.
     * 
     * @param string $name The display-name for the recipient, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function addBcc($addr, $name = null)
    {
        $this->_rcpt['Bcc'][$addr] = $name;
        return $this;
    }
    
    /**
     * 
     * Sets the "Bcc:" address recipient, removing previous "Bcc:" recipients.
     * 
     * @param string $addr The email address of the recipient.
     * 
     * @param string $name The display-name for the recipient, if any.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setBcc($addr, $name = null)
    {
        $this->_rcpt['Bcc'] = array($addr => $name);
        return $this;
    }
    
    /**
     * 
     * Returns an array of all recipient addresses.
     * 
     * @param string $type The recipient type to return: 'to', 'cc', or 'bcc'.
     * If empty (the default) will return all recipient addresses.
     * 
     * @return array A sequential array of recipient addresses.
     * 
     */
    public function getRcpt($type = null)
    {
        $type = ucfirst(trim($type));
        $list = array('To', 'Cc', 'Bcc');
        if ($type && in_array($type, $list)) {
            // just addresses of this type
            return array_keys($this->_rcpt[$type]);
        } elseif (! $type) {
            // no type, return all addresses
            return array_keys(array_merge(
                $this->_rcpt['To'],
                $this->_rcpt['Cc'],
                $this->_rcpt['Bcc']
            ));
        } else {
            // not a recognized type, so no addresses
            return array();
        }
    }

    /**
     * Resets all recipients in To, Cc and Bcc.
     * 
     * @return void
     */
    public function resetRcpt()
    {
        $this->_rcpt['To'] = array();
        $this->_rcpt['Cc'] = array();
        $this->_rcpt['Bcc'] = array();
    }
    
    /**
     * 
     * Sets the subject of the message.
     * 
     * @param string $subject The subject line for the message.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setSubject($subject)
    {
        $this->_subject = $subject;
        return $this;
    }
    
    /**
     * 
     * Returns the message subject.
     * 
     * @return string
     * 
     */
    public function getSubject()
    {
        return $this->_subject;
    }
    
    /**
     * 
     * Sets the part for the plain-text portion of this message.
     * 
     * @param string $text The plain-text message.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setText($text)
    {
        // create the part
        $part = Solar::factory('Solar_Mail_Message_Part');
        $part->setContent($text);
        $part->setCrlf($this->_crlf);
        $part->setType('text/plain');
        $part->setCharset($this->_charset);
        $part->setEncoding($this->_encoding);
        $part->setDisposition('inline');
        
        // keep it
        $this->_text = $part;
        
        // done!
        return $this;
    }
    
    /**
     * 
     * Returns the Solar_Mail_Message_Part for the plain-text portion.
     * 
     * @return Solar_Mail_Message_Part
     * 
     */
    public function getText()
    {
        return $this->_text;
    }
    
    /**
     * 
     * Sets the part for the HTML portion of this message.
     * 
     * @param string $html The HTML message.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function setHtml($html)
    {
        // create the part
        $part = Solar::factory('Solar_Mail_Message_Part');
        $part->setContent($html);
        $part->setCrlf($this->_crlf);
        $part->setType('text/html');
        $part->setCharset($this->_charset);
        $part->setEncoding($this->_encoding);
        $part->setDisposition('inline');
        
        // keep it
        $this->_html = $part;
        
        // done!
        return $this;
    }
    
    /**
     * 
     * Returns the Solar_Mail_Message_Part for the HTML portion.
     * 
     * @return Solar_Mail_Message_Part
     * 
     */
    public function getHtml()
    {
        return $this->_html;
    }
    
    /**
     * 
     * Attaches a Solar_Mail_Message_Part to the message.
     * 
     * @param Solar_Mail_Message_Part $part The part to add as an attachment.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function attachPart($part)
    {
        $this->_atch[] = $part;
        return $this;
    }
    
    /**
     * 
     * Attaches a file to the message.
     * 
     * @param string $file The absolute path and filename to attach.
     * 
     * @param string $type The Content-Type to use for the file. If empty,
     * uses the Solar_Mail_Message_Part default $type.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function attachFile($file, $type = null)
    {
        $part = Solar::factory('Solar_Mail_Message_Part');
        $part->setContent(file_get_contents($file));
        $part->setCrlf($this->_crlf);
        $part->setFilename(basename($file));
        
        if ($type) {
            $part->setType($type);
        }
        
        $this->_atch[] = $part;
        
        return $this;
    }
    
    /**
     * 
     * Adds a custom header to the message.
     * 
     * Canonicalizes the label, and strips CR/LF from the value, to help
     * prevent header injections.
     * 
     * If you need to wrap lines in the header value, don't worry; the header
     * encoding routine will wrap them for you.
     * 
     * @param string $label The header label.
     * 
     * @param string $value The header value.
     * 
     * @param bool $replace If true, resets all headers of the same label so
     * that this is the only value for that header.
     * 
     * @return Solar_Mail_Message This object.
     * 
     */
    public function addHeader($label, $value, $replace = true)
    {
        // sanitize the header label
        $label = Solar_Mime::headerLabel($label);
        
        // not allowed to add headers for these labels
        $list = array('to', 'cc', 'bcc', 'from', 'subject', 'return-path',
            'content-type', 'mime-version', 'content-transfer-encoding');
        if (in_array(strtolower($label), $list)) {
            throw $this->_exception('ERR_USE_OTHER_METHOD', array(
                'key' => $label,
            ));
        }
        
        // if replacing, or not already set, reset to a blank array
        if ($replace || empty($this->_headers[$label])) {
            $this->_headers[$label] = array();
        }
        
        // save the label and value
        $this->_headers[$label][] = $value;
        
        // done!
        return $this;
    }
    
    /**
     * 
     * Fetches all the headers of this message as a sequential array.
     * 
     * Each element is itself sequential array, where element 0 is the
     * header label, and element 1 is the encoded header value.
     * 
     * @return array
     * 
     */
    public function fetchHeaders()
    {
        // the array of headers to return
        $headers = array();
        
        // Return-Path: (alternatively, the From: address)
        if ($this->_return_path) {
            $headers[] = array('Return-Path', "<{$this->_return_path}>");
        } else {
            $headers[] = array('Return-Path', "<{$this->_from[0]}>");
        }
        
        // Date:
        $headers[] = array('Date', date('r'));

        // From:
        $value = "<{$this->_from[0]}>";
        if ($this->_from[1]) {
            $value = '"' . $this->_from[1] . '" ' . $value;
        }
        $headers[] = array("From", $value);
        
        // Reply-To: (optional)
        if ($this->_reply_to[0]) {
            $value = "<{$this->_reply_to[0]}>";
            if ($this->_reply_to[1]) {
                $value = '"' . $this->_reply_to[1] . '" ' . $value;
            }
            $headers[] = array("Reply-To", $value);
        }
        
        // To:, Cc:, Bcc:
        foreach ($this->_rcpt as $label => $rcpt) {
            foreach ($rcpt as $addr => $name) {
                $value = "<$addr>";
                if ($name) {
                    $value = '"' . $name . '" ' . $value;
                }
                $headers[] = array($label, $value);
            }
        }
        
        // Subject:
        $headers[] = array('Subject', $this->_subject);
        
        // Mime-Version:
        $headers[] = array('Mime-Version', '1.0');
        
        // Determine the content type and transfer encoding.
        // Default is no transfer encoding.
        $encoding = null;
        if ($this->_text && $this->_html && ! $this->_atch) {
            
            // both text and html, but no attachments: multipart/alternative
            $value = 'multipart/alternative; '
                   . 'charset="' . $this->_charset . '"; '
                   . $this->_crlf . " "
                   . 'boundary="' . $this->_boundary . '"';
            
        } elseif ($this->_atch) {
            
            // has attachments, use multipart/mixed
            $value = 'multipart/mixed; '
                   . 'charset="' . $this->_charset . '"; '
                   . $this->_crlf . " "
                   . 'boundary="' . $this->_boundary . '"';
            
        } elseif ($this->_html) {
            
            // no attachments, html only
            $value = 'text/html; '
                   . 'charset="' . $this->_charset . '"';
            
            // use the part's encoding
            $encoding = $this->_html->getEncoding();
            
        } elseif ($this->_text) {
            
            // no attachments, text only
            $value = 'text/plain; '
                   . 'charset="' . $this->_charset . '"';
            
            // use the part's encoding
            $encoding = $this->_text->getEncoding();
            
        } else {
            // final fallback
            $value = 'text/plain; '
                   . 'charset="' . $this->_charset . '"';
        }
        
        // Content-Type:
        $headers[] = array('Content-Type', $value);
        
        // Content-Transfer-Encoding:
        if ($encoding) {
            $headers[] = array('Content-Transfer-Encoding', $encoding);
        }
        
        // encode all the headers so far
        foreach ($headers as $key => $val) {
            // val[0] is the label, val[1] is the value
            $val[0] = Solar_Mime::headerLabel($val[0]);
            $headers[$key][1] = Solar_Mime::headerValue(
                $val[0],
                $val[1],
                $this->_charset,
                $this->_crlf
            );
        }
        
        // add and encode custom headers
        foreach ($this->_headers as $label => $list) {
            $label = Solar_Mime::headerLabel($label);
            foreach ($list as $value) {
                $headers[] = array(
                    $label,
                    Solar_Mime::headerValue(
                        $label,
                        $value,
                        $this->_charset,
                        $this->_crlf
                    ),
                );
            }
        }
        
        // done!
        return $headers;
    }
    
    /**
     * 
     * Fetches all the content parts of this message as a string.
     * 
     * See notes here:
     * <http://www.webcheatsheet.com/php/send_email_text_html_attachment.php#attachment>
     * 
     * If we have text *and* html, and attachments, the text and html are 
     * wrapped in their own multipart/alternative subpart, then the message as
     * a whole is built as multipart/mixed.
     * 
     * If we have text *or* html, and attachments, we build as multipart/mixed.
     * 
     * If we have text *or* html, no attachments, we build as a single part.
     * 
     * If we have only attachments, we build as a single part if there's one
     * attachment, or as multipart/mixed if there are more than one.
     * 
     * @return string
     * 
     */
    public function fetchContent()
    {
        // build a stack of all parts for the message: text, html, and
        // attachments
        $parts = array();
        
        // special treatment if we have text **and** html **and** attachments.
        if ($this->_text && $this->_html && $this->_atch) {
            
            // create a separate part to hold only the text and html as
            // alternatives, to keep the attachments separate.  otherwise
            // the text, html, and atches *all* show up in the email inline,
            // when we just want *either* the text *or* the html to show.
            // 
            // @todo this is kind of dumb; we should make the Message_Part be
            // smart enough to handle sub-parts and set up its own boundaries.
            $boundary = '____' . hash('md5', $this->_boundary . uniqid());
            $alt = Solar::factory('Solar_Mail_Message_Part');
            $alt->setCrlf($this->_crlf);
            $alt->setEncoding('7bit');
            $alt->setDisposition(null);
            $alt->setType('multipart/alternative');
            $alt->setCharset($this->_charset);
            $alt->setBoundary($boundary);
            $alt->setContent(ltrim($this->_boundarySep($boundary))
                           . $this->_text->fetch()
                           . $this->_boundarySep($boundary)
                           . $this->_html->fetch()
                           . $this->_boundaryEnd($boundary));
            
            // add the combined text/html alternative part
            $parts[] = $alt;
            
        } else {
            // we have *either* text *or* html, and possibly some attachments.
            // no need to wrap the main-message part, just show it inline.
            //
            // add the text part, if it exists
            if ($this->_text) {
                $parts[] = $this->_text;
            }
        
            // add the html part, if it exists
            if ($this->_html) {
                $parts[] = $this->_html;
            }
        }
        
        // add all the attachments
        $parts = array_merge($parts, $this->_atch);
        
        // we need at least *one* part to send
        if (! $parts) {
            throw $this->_exception('ERR_NO_PARTS');
        }
        
        // is this multi-part?
        if (count($parts) == 1) {
            // no, so it's easy to build
            $content = $parts[0]->fetchContent();
        } else {
            
            // multiple parts.
            // add a warning message ...
            $content = 'This is a message in MIME format. If you see this, '
                     . $this->_crlf
                     . 'your mail reader does not support the MIME format.'
                     . $this->_crlf;
            
            // then each of the parts with a boundary separator
            foreach ($parts as $part) {
                $content .= $this->_boundarySep()
                          . $part->fetch();
            }
        
            // add a boundary ending, and we're done
            $content .= $this->_boundaryEnd();
        }
        
        return trim($content);
    }
    
    /**
     * 
     * If a transport dependency has been injected, use it to send this email.
     * 
     * @return bool True on success, false on failure.
     * 
     * @throws Solar_Mail_Message_Exception_NoTransport
     * 
     */
    public function send()
    {
        if (! $this->_transport) {
            throw $this->_exception('ERR_NO_TRANSPORT');
        }
        
        return $this->_transport->send($this);
    }
    
    /**
     * 
     * Returns a boundary-line separator.
     * 
     * @param string $str The boundary text; if empty, uses $this->_boundary.
     * 
     * @return string
     * 
     */
    protected function _boundarySep($str = null)
    {
        if (! $str) {
            $str = $this->_boundary;
        }
        return "{$this->_crlf}--{$str}{$this->_crlf}";
    }
    
    /**
     * 
     * Returns a boundary-line ending.
     * 
     * @param string $str The boundary text; if empty, uses $this->_boundary.
     * 
     * @return string
     * 
     */
    protected function _boundaryEnd($str = null)
    {
        if (! $str) {
            $str = $this->_boundary;
        }
        return "{$this->_crlf}--{$str}--{$this->_crlf}";
    }
}
Return current item: SolarPHP