Add Horde_Mail package.
authorMichael M Slusarz <slusarz@curecanti.org>
Wed, 12 May 2010 01:07:32 +0000 (19:07 -0600)
committerMichael M Slusarz <slusarz@curecanti.org>
Wed, 12 May 2010 05:49:58 +0000 (23:49 -0600)
Adds the following features:
 * Allows a stream to be passed in.
 * Allows raw headertext to be used in the outgoing messages (required
   for things like message redirection pursuant to RFC 5322 [3.6.6]).
 * Native PHP 5 code.
 * Provides more comprehensive sendmail error messages.
 * Uses Exceptions instead of PEAR_Errors.

13 files changed:
framework/Mail/lib/Horde/Mail.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Driver.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Exception.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Mail.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Mock.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Null.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Rfc822.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Sendmail.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Smtp.php [new file with mode: 0644]
framework/Mail/lib/Horde/Mail/Smtpmx.php [new file with mode: 0644]
framework/Mail/package.xml [new file with mode: 0644]
framework/Mail/test/Horde/Mail/AllTests.php [new file with mode: 0644]
framework/Mail/test/Horde/Mail/ParseTest.php [new file with mode: 0644]

diff --git a/framework/Mail/lib/Horde/Mail.php b/framework/Mail/lib/Horde/Mail.php
new file mode 100644 (file)
index 0000000..44d1797
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Horde's Mail interface.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2002-2007, Richard Heyes
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category  Horde
+ * @package   Mail
+ * @author    Chuck Hagenbuch <chuck@horde.org>
+ * @author    Michael Slusarz <slusarz@horde.org>
+ * @copyright 1997-2010 Chuck Hagenbuch
+ * @copyright 2010 Michael Slusarz
+ * @license   http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * The Mail interface.
+ *
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail
+{
+    /**
+     * Returns a Horde_Mail_Driver:: object.
+     *
+     * @param string $driver  The driver to instantiate.
+     * @param array $params   The parameters to pass to the object.
+     *
+     * @return Horde_Mail_Driver  The driver instance.
+     * @throws Horde_Mail_Exception
+     */
+    static public function factory($driver, $params = array())
+    {
+        $class = __CLASS__ . '_' . ucfirst($driver);
+
+        if (class_exists($class)) {
+            return new $class($params);
+        }
+
+        throw new Horde_Mail_Exception('Unable to find class for driver ' . $driver);
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Driver.php b/framework/Mail/lib/Horde/Mail/Driver.php
new file mode 100644 (file)
index 0000000..fb0f088
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+/**
+ * Mail driver base class.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2002-2007, Richard Heyes
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category    Mail
+ * @package     Mail
+ * @author      Chuck Hagenbuch <chuck@horde.org>
+ * @author      Michael Slusarz <slusarz@horde.org>
+ * @copyright   1997-2010 Chuck Hagenbuch
+ * @copyright   2010 Michael Slusarz
+ * @license     http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * Mail driver base class.
+ *
+ * @access public
+ * @version $Revision: 294747 $
+ * @package Mail
+ */
+abstract class Horde_Mail_Driver
+{
+    /**
+     * Line terminator used for separating header lines.
+     *
+     * @var string
+     */
+    public $sep = "\r\n";
+
+    /**
+     * Configuration parameters.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * Send a message.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    abstract public function send($recipients, array $headers, $body);
+
+    /**
+     * Take an array of mail headers and return a string containing text
+     * usable in sending a message.
+     *
+     * @param array $headers  The array of headers to prepare, in an
+     *                        associative array, where the array key is the
+     *                        header name (ie, 'Subject'), and the array value
+     *                        is the header value (ie, 'test'). The header
+     *                        produced from those values would be 'Subject:
+     *                        test'.
+     *                        If the '_raw' key exists, the value of this key
+     *                        will be used as the exact text for sending the
+     *                        message.
+     *
+     * @return mixed  Returns false if it encounters a bad address; otherwise
+     *                returns an array containing two elements: Any From:
+     *                address found in the headers, and the plain text version
+     *                of the headers.
+     * @throws Horde_Mail_Exception
+     */
+    public function prepareHeaders(array $headers)
+    {
+        $lines = array();
+        $from = null;
+
+        $parser = new Horde_Mail_Rfc822();
+
+        foreach ($headers as $key => $value) {
+            if (strcasecmp($key, 'From') === 0) {
+                $addresses = $parser->parseAddressList($value, array(
+                    'nest_groups' => false,
+                ));
+                $from = $addresses[0]->mailbox . '@' . $addresses[0]->host;
+
+                // Reject envelope From: addresses with spaces.
+                if (strstr($from, ' ')) {
+                    return false;
+                }
+
+                $lines[] = $key . ': ' . $value;
+            } elseif (strcasecmp($key, 'Received') === 0) {
+                $received = array();
+                if (!is_array($value)) {
+                    $value = array($value);
+                }
+
+                foreach ($value as $line) {
+                    $received[] = $key . ': ' . $line;
+                }
+
+                // Put Received: headers at the top.  Spam detectors often
+                // flag messages with Received: headers after the Subject:
+                // as spam.
+                $lines = array_merge($received, $lines);
+            } else {
+                // If $value is an array (i.e., a list of addresses), convert
+                // it to a comma-delimited string of its elements (addresses).
+                if (is_array($value)) {
+                    $value = implode(', ', $value);
+                }
+                $lines[] = $key . ': ' . $value;
+            }
+        }
+
+        return array($from, isset($headers['_raw']) ? $headers['_raw'] : join($this->sep, $lines));
+    }
+
+    /**
+     * Take a set of recipients and parse them, returning an array of bare
+     * addresses (forward paths) that can be passed to sendmail or an SMTP
+     * server with the 'RCPT TO:' command.
+     *
+     * @param mixed  Either a comma-separated list of recipients (RFC822
+     *               compliant), or an array of recipients, each RFC822 valid.
+     *
+     * @return array  Forward paths (bare addresses).
+     * @throws Horde_Mail_Exception
+     */
+    public function parseRecipients($recipients)
+    {
+        // if we're passed an array, assume addresses are valid and
+        // implode them before parsing.
+        if (is_array($recipients)) {
+            $recipients = implode(', ', $recipients);
+        }
+
+        // Parse recipients, leaving out all personal info. This is
+        // for smtp recipients, etc. All relevant personal information
+        // should already be in the headers.
+        $parser = new Horde_Mail_Rfc822();
+        $addresses = $parser->parseAddressList($recipients, array(
+            'nest_groups' => false
+        ));
+
+        $recipients = array();
+        if (is_array($addresses)) {
+            foreach ($addresses as $ob) {
+                $recipients[] = $ob->mailbox . '@' . $ob->host;
+            }
+        }
+
+        return $recipients;
+    }
+
+    /**
+     * Sanitize an array of mail headers by removing any additional header
+     * strings present in a legitimate header's value.  The goal of this
+     * filter is to prevent mail injection attacks.
+     *
+     * @param array $headers  The associative array of headers to sanitize.
+     *
+     * @return array  The sanitized headers.
+     */
+    protected function _sanitizeHeaders($headers)
+    {
+        foreach (array_keys($headers) as $key) {
+            $headers[$key] = preg_replace('=((<CR>|<LF>|0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', null, $headers[$key]);
+        }
+
+        return $headers;
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Exception.php b/framework/Mail/lib/Horde/Mail/Exception.php
new file mode 100644 (file)
index 0000000..eb2c0c5
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Exception handler for the Horde_Mail class.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (BSD). If you
+ * did not receive this file, see
+ * http://opensource.org/licenses/bsd-license.php
+ *
+ * @author   Michael Slusarz <slusarz@horde.org>
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Exception extends Horde_Exception_Prior
+{
+}
diff --git a/framework/Mail/lib/Horde/Mail/Mail.php b/framework/Mail/lib/Horde/Mail/Mail.php
new file mode 100644 (file)
index 0000000..1fdd83c
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Internal PHP-mail() interface.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2010 Chuck Hagenbuch
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category  Horde
+ * @package   Mail
+ * @author    Chuck Hagenbuch <chuck@horde.org>
+ * @author    Michael Slusarz <slusarz@horde.org>
+ * @copyright 2010 Chuck Hagenbuch
+ * @copyright 2010 Michael Slusarz
+ * @license   http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * Internal PHP-mail() interface.
+ *
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Mail extends Horde_Mail_Driver
+{
+    /**
+     * Constructor.
+     *
+     * @param array $params  Additional parameters:
+     * <pre>
+     * 'args' - (string) Extra arguments for the mail() function.
+     * </pre>
+     */
+    public function __construct(array $params = array())
+    {
+        $this->_params = array_merge($this->_params, $params);
+
+        /* Because the mail() function may pass headers as command
+         * line arguments, we can't guarantee the use of the standard
+         * "\r\n" separator.  Instead, we use the system's native line
+         * separator. */
+        $this->sep = defined('PHP_EOL')
+            ? PHP_EOL
+            : (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n";
+    }
+
+    /**
+     * Send a message.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    public function send($recipients, array $headers, $body)
+    {
+        $headers = $this->_sanitizeHeaders($headers);
+
+        // If we're passed an array of recipients, implode it.
+        if (is_array($recipients)) {
+            $recipients = array_map('trim', implode(',', $recipients));
+        }
+
+        $subject = '';
+
+        foreach (array_keys($headers) as $hdr) {
+            if (strcasecmp($hdr, 'Subject') === 0) {
+                // Get the Subject out of the headers array so that we can
+                // pass it as a separate argument to mail().
+                $subject = $headers[$hdr];
+                unset($headers[$hdr]);
+            } elseif (strcasecmp($hdr, 'To') === 0) {
+                // Remove the To: header.  The mail() function will add its
+                // own To: header based on the contents of $recipients.
+                unset($headers[$hdr]);
+            }
+        }
+
+        // Flatten the headers out.
+        list(, $text_headers) = $this->prepareHeaders($headers);
+
+        // mail() requires a string for $body. If resource, need to convert
+        // to a string.
+        if (is_resource($body)) {
+            $body_str = '';
+            rewind($body);
+            while (!feof($body)) {
+                $body_str .= fread($body, 8192);
+            }
+            $body = $body_str;
+        }
+
+        // We only use mail()'s optional fifth parameter if the additional
+        // parameters have been provided and we're not running in safe mode.
+        if (empty($this->_params) || ini_get('safe_mode')) {
+            $result = mail($recipients, $subject, $body, $text_headers);
+        } else {
+            $result = mail($recipients, $subject, $body, $text_headers, isset($this->_params['args']) ? $this->_params['args'] : '');
+        }
+
+        // If the mail() function returned failure, we need to create an
+        // Exception and return it instead of the boolean result.
+        if ($result === false) {
+            throw new Horde_Mail_Exception('mail() returned failure.');
+        }
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Mock.php b/framework/Mail/lib/Horde/Mail/Mock.php
new file mode 100644 (file)
index 0000000..6e68c31
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Mock mail driver.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2010 Chuck Hagenbuch
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category  Horde
+ * @package   Mail
+ * @author    Chuck Hagenbuch <chuck@horde.org>
+ * @copyright 2010 Chuck Hagenbuch
+ * @license   http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * Mock implementation, for testing.
+ *
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Mock extends Horde_Mail_Driver
+{
+    /**
+     * Array of messages that have been sent with the mock.
+     *
+     * @var array
+     */
+    public $sentMessages = array();
+
+    /**
+     * Callback before sending mail.
+     *
+     * @var callback
+     */
+    protected $_preSendCallback;
+
+    /**
+     * Callback after sending mai.
+     *
+     * @var callback
+     */
+    protected $_postSendCallback;
+
+    /**
+     * Constructor.
+     *
+     * @param array  Optional parameters:
+     * <pre>
+     * 'preSendCallback' - (callback) Called before an email would be sent.
+     * 'postSendCallback' - (callback) Called after an email would have been
+     *                      sent.
+     * </pre>
+     */
+    public function __construct(array $params = array())
+    {
+        if (isset($params['preSendCallback']) &&
+            is_callable($params['preSendCallback'])) {
+            $this->_preSendCallback = $params['preSendCallback'];
+        }
+
+        if (isset($params['postSendCallback']) &&
+            is_callable($params['postSendCallback'])) {
+            $this->_postSendCallback = $params['postSendCallback'];
+        }
+    }
+
+    /**
+     * Send a message. Silently discards all mail.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    public function send($recipients, array $headers, $body)
+    {
+        if ($this->_preSendCallback) {
+            call_user_func_array($this->_preSendCallback, array($this, $recipients, $headers, $body));
+        }
+
+        $headers = $this->_sanitizeHeaders($headers);
+        list(, $text_headers) = $this->prepareHeaders($headers);
+
+        $this->sentMessages[] = array(
+            'body' => $body,
+            'headers' => $headers,
+            'header_text' => $text_headers,
+            'recipients' => $recipients
+        );
+
+        if ($this->_postSendCallback) {
+            call_user_func_array($this->_postSendCallback, array($this, $recipients, $headers, $body));
+        }
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Null.php b/framework/Mail/lib/Horde/Mail/Null.php
new file mode 100644 (file)
index 0000000..bf03470
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Null implementation of the mail interface.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2010 Phil Kernick
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category    Horde
+ * @package     Mail
+ * @author      Phil Kernick <philk@rotfl.com.au>
+ * @copyright   2010 Phil Kernick
+ * @license     http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * Null implementation of the mail interface.
+ *
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Null extends Horde_Mail_Driver
+{
+    /**
+     * Send a message.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    public function send($recipients, array $headers, $body)
+    {
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Rfc822.php b/framework/Mail/lib/Horde/Mail/Rfc822.php
new file mode 100644 (file)
index 0000000..2d5b584
--- /dev/null
@@ -0,0 +1,921 @@
+<?php
+/**
+ * RFC 822 Email address list validation Utility
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2001-2010, Richard Heyes
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category    Horde
+ * @package     Mail
+ * @author      Richard Heyes <richard@phpguru.org>
+ * @author      Chuck Hagenbuch <chuck@horde.org
+ * @copyright   2001-2010 Richard Heyes
+ * @license     http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * RFC 822 Email address list validation Utility
+ *
+ * What is it?
+ *
+ * This class will take an address string, and parse it into it's consituent
+ * parts, be that either addresses, groups, or combinations. Nested groups
+ * are not supported. The structure it returns is pretty straight forward,
+ * and is similar to that provided by the imap_rfc822_parse_adrlist().
+ *
+ * @author   Richard Heyes <richard@phpguru.org>
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @category Horde
+ * @license  BSD
+ * @package  Mail
+ */
+class Horde_Mail_Rfc822
+{
+    /**
+     * The number of groups that have been found in the address list.
+     *
+     * @var integer
+     */
+    public $num_groups = 0;
+
+    /**
+     * The default domain to use for unqualified addresses.
+     *
+     * @var string
+     */
+    protected $default_domain = 'localhost';
+
+    /**
+     * Should we return a nested array showing groups, or flatten everything?
+     *
+     * @var boolean
+     */
+    protected $nestGroups = true;
+
+    /**
+     * Whether or not to validate atoms for non-ascii characters.
+     *
+     * @var boolean
+     */
+    protected $validate = true;
+
+    /**
+     * The array of raw addresses built up as we parse.
+     *
+     * @var array
+     */
+    protected $addresses = array();
+
+    /**
+     * The current error message, if any.
+     *
+     * @var string
+     */
+    protected $error = null;
+
+    /**
+     * An internal counter/pointer.
+     *
+     * @var integer
+     */
+    protected $index = null;
+
+    /**
+    * A limit after which processing stops
+    *
+    * @var integer
+    */
+    protected $limit = null;
+
+    /**
+     * Starts the whole process.
+     *
+     * @param string $address  The address(es) to validate.
+     * @param array $opts      Additional options:
+     * <pre>
+     * 'default_domain' - (string) Default domain/host etc.
+     *                    DEFAULT: localhost
+     * 'limit' - (integer) TODO
+     *           DEFAULT: NONE
+     * 'nest_groups' - (boolean) Whether to return the structure with groups
+     *                 nested for easier viewing.
+     *                 DEFAULT: true
+     * 'validate' - (boolean) Whether to validate atoms.
+     *              DEFAULT: true
+     * </pre>
+     *
+     * @return array  A structured array of addresses.
+     * @throws Horde_Mail_Exception
+     */
+    public function parseAddressList($address = null, array $opts = array())
+    {
+        if (isset($opts['default_domain'])) {
+            $this->default_domain = $opts['default_domain'];
+        }
+        if (isset($opts['nest_groups'])) {
+            $this->nestGroups = $opts['nest_groups'];
+        }
+        if (isset($opts['validate'])) {
+            $this->validate = $opts['validate'];
+        }
+        if (isset($opts['limit'])) {
+            $this->limit = $opts['limit'];
+        }
+
+        $this->addresses = $structure = array();
+        $this->error = $this->index = null;
+
+        // Unfold any long lines in $address.
+        $address = preg_replace(array('/\r?\n/', '/\r\n(\t| )+/'), array("\r\n", ' '), $address);
+
+        while ($address = $this->_splitAddresses($address));
+
+        if ($address === false || isset($this->error)) {
+            throw new Horde_Mail_Exception($this->error);
+        }
+
+        // Validate each address individually.  If we encounter an invalid
+        // address, stop iterating and return an error immediately.
+        foreach ($this->addresses as $address) {
+            $valid = $this->_validateAddress($address);
+
+            if ($valid === false || isset($this->error)) {
+                throw new Horde_Mail_Exception($this->error);
+            }
+
+            if (!$this->nestGroups) {
+                $structure = array_merge($structure, $valid);
+            } else {
+                $structure[] = $valid;
+            }
+        }
+
+        return $structure;
+    }
+
+    /**
+     * Splits an address into separate addresses.
+     *
+     * @param string $address  The addresses to split.
+     *
+     * @return boolean  Success or failure.
+     */
+    protected function _splitAddresses($address)
+    {
+        if (!empty($this->limit) &&
+            (count($this->addresses) == $this->limit)) {
+            return '';
+        }
+
+        if ($this->_isGroup($address) && !isset($this->error)) {
+            $split_char = ';';
+            $is_group = true;
+        } elseif (!isset($this->error)) {
+            $split_char = ',';
+            $is_group = false;
+        } elseif (isset($this->error)) {
+            return false;
+        }
+
+        // Split the string based on the above ten or so lines.
+        $parts = explode($split_char, $address);
+        $string = $this->_splitCheck($parts, $split_char);
+
+        if ($is_group) {
+            // If $string does not contain a colon outside of
+            // brackets/quotes etc then something's fubar.
+
+            // First check there's a colon at all:
+            if (strpos($string, ':') === false) {
+                $this->error = 'Invalid address: ' . $string;
+                return false;
+            }
+
+            // Now check it's outside of brackets/quotes:
+            if (!$this->_splitCheck(explode(':', $string), ':')) {
+                return false;
+            }
+
+            // We must have a group at this point, so increase the counter:
+            ++$this->num_groups;
+        }
+
+        // $string now contains the first full address/group.
+        // Add to the addresses array.
+        $this->addresses[] = array(
+            'address' => trim($string),
+            'group'   => $is_group
+        );
+
+        // Remove the now stored address from the initial line, the +1
+        // is to account for the explode character.
+        $address = trim(substr($address, strlen($string) + 1));
+
+        // If the next char is a comma and this was a group, then
+        // there are more addresses, otherwise, if there are any more
+        // chars, then there is another address.
+        if ($is_group && substr($address, 0, 1) == ','){
+            return trim(substr($address, 1));
+        } elseif (strlen($address) > 0) {
+            return $address;
+        } else {
+            return '';
+        }
+
+        // If you got here then something's off
+        return false;
+    }
+
+    /**
+     * Checks for a group at the start of the string.
+     *
+     * @param string $address  The address to check.
+     *
+     * @return boolean  Is there a group at the start of the string?
+     */
+    protected function _isGroup($address)
+    {
+        // First comma not in quotes, angles or escaped:
+        $string = $this->_splitCheck(explode(',', $address), ',');
+
+        // Now we have the first address, we can reliably check for a
+        // group by searching for a colon that's not escaped or in
+        // quotes or angle brackets.
+        if (count($parts = explode(':', $string)) > 1) {
+            $string2 = $this->_splitCheck($parts, ':');
+            return ($string2 !== $string);
+        }
+
+        return false;
+    }
+
+    /**
+     * A common function that will check an exploded string.
+     *
+     * @param array $parts  The exploded string.
+     * @param string $char  The char that was exploded on.
+     *
+     * @return mixed  False if the string contains unclosed quotes/brackets,
+     *                or the string on success.
+     */
+    protected function _splitCheck($parts, $char)
+    {
+        $string = $parts[0];
+
+        for ($i = 0; $i < count($parts); ++$i) {
+            if ($this->_hasUnclosedQuotes($string) ||
+                $this->_hasUnclosedBrackets($string, '<>') ||
+                $this->_hasUnclosedBrackets($string, '[]') ||
+                $this->_hasUnclosedBrackets($string, '()') ||
+                (substr($string, -1) == '\\')) {
+                if (!isset($parts[$i + 1])) {
+                    $this->error = 'Invalid address spec. Unclosed bracket or quotes';
+                    return false;
+                }
+
+                $string = $string . $char . $parts[$i + 1];
+            } else {
+                $this->index = $i;
+                break;
+            }
+        }
+
+        return $string;
+    }
+
+    /**
+     * Checks if a string has unclosed quotes or not.
+     *
+     * @param string $string  The string to check.
+     *
+     * @return boolean  True if there are unclosed quotes inside the string,
+     *                  false otherwise.
+     */
+    protected function _hasUnclosedQuotes($string)
+    {
+        $string = trim($string);
+        $iMax = strlen($string);
+        $in_quote = false;
+        $i = $slashes = 0;
+
+        for (; $i < $iMax; ++$i) {
+            switch ($string[$i]) {
+            case '\\':
+                ++$slashes;
+                break;
+
+            case '"':
+                if ($slashes % 2 == 0) {
+                    $in_quote = !$in_quote;
+                }
+                // Fall through to default action below.
+
+            default:
+                $slashes = 0;
+                break;
+            }
+        }
+
+        return $in_quote;
+    }
+
+    /**
+     * Checks if a string has an unclosed brackets or not. IMPORTANT:
+     * This function handles both angle brackets and square brackets;
+     *
+     * @param string $string  The string to check.
+     * @param string $chars   The characters to check for.
+     *
+     * @return boolean  True if there are unclosed brackets inside the string,
+     *                  false otherwise.
+     */
+    protected function _hasUnclosedBrackets($string, $chars)
+    {
+        $num_angle_start = substr_count($string, $chars[0]);
+        $num_angle_end = substr_count($string, $chars[1]);
+
+        $num_angle_start = $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
+        $num_angle_end = $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
+
+        if ($num_angle_start < $num_angle_end) {
+            $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
+            return false;
+        }
+
+        return ($num_angle_start > $num_angle_end);
+    }
+
+    /**
+     * Sub function that is used only by hasUnclosedBrackets().
+     *
+     * @param string $string  The string to check.
+     * @param integer $num    The number of occurences.
+     * @param string $char    The character to count.
+     *
+     * @return integer The number of occurences of $char in $string, adjusted for backslashes.
+     */
+    protected function _hasUnclosedBracketsSub($string, $num, $char)
+    {
+        $parts = explode($char, $string);
+
+        for ($i = 0, $p = count($parts); $i < $p; ++$i) {
+            if ((substr($parts[$i], -1) == '\\') ||
+                $this->_hasUnclosedQuotes($parts[$i])) {
+                --$num;
+            }
+
+            if (isset($parts[$i + 1])) {
+                $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
+            }
+        }
+
+        return $num;
+    }
+
+    /**
+     * Function to begin checking the address.
+     *
+     * @param string $address  The address to validate.
+     *
+     * @return mixed  False on failure, or a structured array of address
+     *                information on success.
+     */
+    protected function _validateAddress($address)
+    {
+        $is_group = false;
+        $addresses = array();
+
+        if ($address['group']) {
+            $is_group = true;
+
+            // Get the group part of the name
+            $parts = explode(':', $address['address']);
+            $groupname = $this->_splitCheck($parts, ':');
+            $structure = array();
+
+            // And validate the group part of the name.
+            if (!$this->_validatePhrase($groupname)){
+                $this->error = 'Group name did not validate.';
+                return false;
+            } else {
+                // Don't include groups if we are not nesting
+                // them. This avoids returning invalid addresses.
+                if ($this->nestGroups) {
+                    $structure = new stdClass;
+                    $structure->groupname = $groupname;
+                }
+            }
+
+            $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
+        }
+
+        // If a group then split on comma and put into an array.
+        // Otherwise, Just put the whole address in an array.
+        if ($is_group) {
+            while (strlen($address['address']) > 0) {
+                $parts = explode(',', $address['address']);
+                $addresses[] = $this->_splitCheck($parts, ',');
+                $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
+            }
+        } else {
+            $addresses[] = $address['address'];
+        }
+
+        // Check that $addresses is set, if address like this:
+        // Groupname:;
+        // Then errors were appearing.
+        if (!count($addresses)){
+            $this->error = 'Empty group.';
+            return false;
+        }
+
+        // Trim the whitespace from all of the address strings.
+        array_map('trim', $addresses);
+
+        // Validate each mailbox.
+        // Format could be one of: name <geezer@domain.com>
+        //                         geezer@domain.com
+        //                         geezer
+        // ... or any other format valid by RFC 822.
+        for ($i = 0; $i < count($addresses); $i++) {
+            if (!$this->validateMailbox($addresses[$i])) {
+                if (empty($this->error)) {
+                    $this->error = 'Validation failed for: ' . $addresses[$i];
+                }
+                return false;
+            }
+        }
+
+        // Nested format
+        if ($this->nestGroups) {
+            if ($is_group) {
+                $structure->addresses = $addresses;
+            } else {
+                $structure = $addresses[0];
+            }
+
+        // Flat format
+        } else {
+            $structure = $is_group
+                ? array_merge($structure, $addresses)
+                : $addresses;
+        }
+
+        return $structure;
+    }
+
+    /**
+     * Function to validate a phrase.
+     *
+     * @param string $phrase  The phrase to check.
+     *
+     * @return boolean  Success or failure.
+     */
+    protected function _validatePhrase($phrase)
+    {
+        // Splits on one or more Tab or space.
+        $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
+
+        $phrase_parts = array();
+        while (count($parts) > 0) {
+            $phrase_parts[] = $this->_splitCheck($parts, ' ');
+            for ($i = 0; $i < $this->index + 1; ++$i) {
+                array_shift($parts);
+            }
+        }
+
+        foreach ($phrase_parts as $part) {
+            // If quoted string:
+            if (substr($part, 0, 1) == '"') {
+                if (!$this->_validateQuotedString($part)) {
+                    return false;
+                }
+                continue;
+            }
+
+            // Otherwise it's an atom:
+            if (!$this->_validateAtom($part)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Function to validate an atom which from rfc822 is:
+     * atom = 1*<any CHAR except specials, SPACE and CTLs>
+     *
+     * If validation ($this->validate) has been turned off, then
+     * validateAtom() doesn't actually check anything. This is so that you
+     * can split a list of addresses up before encoding personal names
+     * (umlauts, etc.), for example.
+     *
+     * @param string $atom  The string to check.
+     *
+     * @return boolean  Success or failure.
+     */
+    protected function _validateAtom($atom)
+    {
+        if (!$this->validate) {
+            // Validation has been turned off; assume the atom is okay.
+            return true;
+        }
+
+        // Check for any char from ASCII 0 - ASCII 127
+        if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
+            return false;
+        }
+
+        // Check for specials:
+        if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
+            return false;
+        }
+
+        // Check for control characters (ASCII 0-31):
+        if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Function to validate quoted string, which is:
+     * quoted-string = <"> *(qtext/quoted-pair) <">
+     *
+     * @param string $qstring  The string to check
+     *
+     * @return boolean  Success or failure.
+     */
+    protected function _validateQuotedString($qstring)
+    {
+        // Leading and trailing "
+        $qstring = substr($qstring, 1, -1);
+
+        // Perform check, removing quoted characters first.
+        return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
+    }
+
+    /**
+     * Function to validate a mailbox, which is:
+     * mailbox =   addr-spec         ; simple address
+     *           / phrase route-addr ; name and route-addr
+     *
+     * @param string &$mailbox  The string to check.
+     *
+     * @return boolean  Success or failure.
+     */
+    public function validateMailbox(&$mailbox)
+    {
+        $comment = $phrase = '';
+        $comments = array();
+
+        // Catch any RFC822 comments and store them separately.
+        $_mailbox = $mailbox;
+        while (strlen(trim($_mailbox)) > 0) {
+            $parts = explode('(', $_mailbox);
+            $before_comment = $this->_splitCheck($parts, '(');
+            if ($before_comment == $_mailbox) {
+                break;
+            }
+
+            // First char should be a (.
+            $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
+            $parts = explode(')', $comment);
+            $comment = $this->_splitCheck($parts, ')');
+            $comments[] = $comment;
+
+            // +2 is for the brackets
+            $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2);
+        }
+
+        foreach ($comments as $comment) {
+            $mailbox = str_replace("($comment)", '', $mailbox);
+        }
+
+        $mailbox = trim($mailbox);
+
+        // Check for name + route-addr
+        if ((substr($mailbox, -1) == '>') &&
+            (substr($mailbox, 0, 1) != '<')) {
+            $parts = explode('<', $mailbox);
+            $name = $this->_splitCheck($parts, '<');
+
+            $phrase = trim($name);
+            $route_addr = trim(substr($mailbox, strlen($name.'<'), -1));
+
+            if (($this->_validatePhrase($phrase) === false) ||
+                (($route_addr = $this->_validateRouteAddr($route_addr)) === false)) {
+                return false;
+            }
+
+        // Only got addr-spec
+        } else {
+            // First snip angle brackets if present.
+            $addr_spec = ((substr($mailbox, 0, 1) == '<') && (substr($mailbox, -1) == '>'))
+                ? substr($mailbox, 1, -1)
+                : $mailbox;
+
+            if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
+                return false;
+            }
+        }
+
+        // Construct the object that will be returned.
+        $mbox = new stdClass();
+
+        // Add the phrase (even if empty) and comments
+        $mbox->personal = $phrase;
+        $mbox->comment  = isset($comments)
+            ? $comments
+            : array();
+
+        if (isset($route_addr)) {
+            $mbox->mailbox = $route_addr['local_part'];
+            $mbox->host = $route_addr['domain'];
+            $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
+        } else {
+            $mbox->mailbox = $addr_spec['local_part'];
+            $mbox->host = $addr_spec['domain'];
+        }
+
+        $mailbox = $mbox;
+
+        return true;
+    }
+
+    /**
+     * This function validates a route-addr which is:
+     * route-addr = "<" [route] addr-spec ">"
+     *
+     * Angle brackets have already been removed at the point of entering
+     * this function.
+     *
+     * @param string $route_addr  The string to check.
+     *
+     * @return mixed  False on failure, or an array containing validated
+     *                address/route information on success.
+     */
+    protected function _validateRouteAddr($route_addr)
+    {
+        // Check for colon.
+        if (strpos($route_addr, ':') !== false) {
+            $parts = explode(':', $route_addr);
+            $route = $this->_splitCheck($parts, ':');
+        } else {
+            $route = $route_addr;
+        }
+
+        // If $route is same as $route_addr then the colon was in
+        // quotes or brackets or, of course, non existent.
+        if ($route === $route_addr){
+            unset($route);
+            $addr_spec = $route_addr;
+            if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
+                return false;
+            }
+        } else {
+            // Validate route part.
+            if (($route = $this->_validateRoute($route)) === false) {
+                return false;
+            }
+
+            $addr_spec = substr($route_addr, strlen($route . ':'));
+
+            // Validate addr-spec part.
+            if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
+                return false;
+            }
+        }
+
+        $return['adl'] = isset($route)
+            ? $route
+            : '';
+
+        return array_merge($return, $addr_spec);
+    }
+
+    /**
+     * Function to validate a route, which is:
+     * route = 1#("@" domain) ":"
+     *
+     * @param string $route  The string to check.
+     *
+     * @return mixed  False on failure, or the validated $route on success.
+     */
+    protected function _validateRoute($route)
+    {
+        // Split on comma.
+        $domains = explode(',', trim($route));
+
+        foreach ($domains as $domain) {
+            $domain = str_replace('@', '', trim($domain));
+            if (!$this->_validateDomain($domain)) {
+                return false;
+            }
+        }
+
+        return $route;
+    }
+
+    /**
+     * Function to validate a domain, though this is not quite what
+     * you expect of a strict internet domain.
+     *
+     * domain = sub-domain *("." sub-domain)
+     *
+     * @param string $domain  The string to check.
+     *
+     * @return mixed  False on failure, or the validated domain on success.
+     */
+    protected function _validateDomain($domain)
+    {
+        // Note the different use of $subdomains and $sub_domains
+        $subdomains = explode('.', $domain);
+
+        while (count($subdomains) > 0) {
+            $sub_domains[] = $this->_splitCheck($subdomains, '.');
+            for ($i = 0; $i < $this->index + 1; ++$i) {
+                array_shift($subdomains);
+            }
+        }
+
+        foreach ($sub_domains as $sub_domain) {
+            if (!$this->_validateSubdomain(trim($sub_domain))) {
+                return false;
+            }
+        }
+
+        // Managed to get here, so return input.
+        return $domain;
+    }
+
+    /**
+     * Function to validate a subdomain:
+     *   subdomain = domain-ref / domain-literal
+     *
+     * @param string $subdomain  The string to check.
+     *
+     * @return boolean  Success or failure.
+     */
+    protected function _validateSubdomain($subdomain)
+    {
+        return !((preg_match('|^\[(.*)]$|', $subdomain, $arr) &&
+                 !$this->_validateDliteral($arr[1])) ||
+                 !$this->_validateAtom($subdomain));
+    }
+
+    /**
+     * Function to validate a domain literal:
+     *   domain-literal =  "[" *(dtext / quoted-pair) "]"
+     *
+     * @param string $dliteral  The string to check.
+     *
+     * @return boolean  Success or failure.
+     */
+    protected function _validateDliteral($dliteral)
+    {
+        return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) &&
+               ($matches[1] != '\\');
+    }
+
+    /**
+     * Function to validate an addr-spec.
+     *
+     * addr-spec = local-part "@" domain
+     *
+     * @param string $addr_spec  The string to check.
+     *
+     * @return mixed  False on failure, or the validated addr-spec on success.
+     */
+    protected function _validateAddrSpec($addr_spec)
+    {
+        $addr_spec = trim($addr_spec);
+
+        // Split on @ sign if there is one.
+        if (strpos($addr_spec, '@') !== false) {
+            $parts      = explode('@', $addr_spec);
+            $local_part = $this->_splitCheck($parts, '@');
+            $domain     = substr($addr_spec, strlen($local_part . '@'));
+
+        // No @ sign so assume the default domain.
+        } else {
+            $local_part = $addr_spec;
+            $domain     = $this->default_domain;
+        }
+
+        if ((($local_part = $this->_validateLocalPart($local_part)) === false) ||
+            (($domain     = $this->_validateDomain($domain)) === false)) {
+            return false;
+        }
+
+        // Got here so return successful.
+        return array(
+            'domain' => $domain,
+            'local_part' => $local_part
+        );
+    }
+
+    /**
+     * Function to validate the local part of an address:
+     *   local-part = word *("." word)
+     *
+     * @param string $local_part  TODO
+     *
+     * @return mixed  False on failure, or the validated local part on
+     *                success.
+     */
+    protected function _validateLocalPart($local_part)
+    {
+        $parts = explode('.', $local_part);
+        $words = array();
+
+        // Split the local_part into words.
+        while (count($parts) > 0){
+            $words[] = $this->_splitCheck($parts, '.');
+            for ($i = 0; $i < $this->index + 1; ++$i) {
+                array_shift($parts);
+            }
+        }
+
+        // Validate each word.
+        foreach ($words as $word) {
+            // If this word contains an unquoted space, it is invalid. (6.2.4)
+            if ((strpos($word, ' ') && ($word[0] !== '"')) ||
+                ($this->_validatePhrase(trim($word)) === false)) {
+                return false;
+            }
+        }
+
+        // Managed to get here, so return the input.
+        return $local_part;
+    }
+
+    /**
+     * Returns an approximate count of how many addresses are in the
+     * given string. This is APPROXIMATE as it only splits based on a
+     * comma which has no preceding backslash. Could be useful as
+     * large amounts of addresses will end up producing *large*
+     * structures when used with parseAddressList().
+     *
+     * @param string $data  Addresses to count.
+     *
+     * @return integer  Approximate count.
+     */
+    public function approximateCount($data)
+    {
+        return count(preg_split('/(?<!\\\\),/', $data));
+    }
+
+    /**
+     * This is a email validating function separate to the rest of the
+     * class. It simply validates whether an email is of the common
+     * internet form: <user>@<domain>. This can be sufficient for most
+     * people. Optional stricter mode can be utilised which restricts
+     * mailbox characters allowed to alphanumeric, full stop, hyphen
+     * and underscore.
+     *
+     * @param string $data     Address to check.
+     * @param boolean $strict  Optional stricter mode.
+     *
+     * @return mixed  False if it fails, an indexed array username/domain if
+     *                it matches.
+     */
+    public function isValidInetAddress($data, $strict = false)
+    {
+        $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
+
+        return preg_match($regex, trim($data), $matches)
+            ? array($matches[1], $matches[2])
+            : false;
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Sendmail.php b/framework/Mail/lib/Horde/Mail/Sendmail.php
new file mode 100644 (file)
index 0000000..08c01a3
--- /dev/null
@@ -0,0 +1,247 @@
+<?php
+/**
+ * Sendmail interface.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2010 Chuck Hagenbuch
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category  Horde
+ * @package   Mail
+ * @author    Chuck Hagenbuch <chuck@horde.org>
+ * @author    Michael Slusarz <slusarz@horde.org>
+ * @copyright 2010 Chuck Hagenbuch
+ * @copyright 2010 Michael Slusarz
+ * @license   http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * Sendmail interface.
+ *
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Sendmail extends Horde_Mail_Driver
+{
+    /**
+     * Any extra command-line parameters to pass to the sendmail or
+     * sendmail wrapper binary.
+     *
+     * @var string
+     */
+    protected $_sendmailArgs = '-i';
+
+    /**
+     * The location of the sendmail or sendmail wrapper binary on the
+     * filesystem.
+     *
+     * @var string
+     */
+    protected $_sendmailPath = '/usr/sbin/sendmail';
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  Additional parameters:
+     * <pre>
+     * 'sendmail_args' - (string) Any extra parameters to pass to the sendmail
+     *                   or sendmail wrapper binary.
+     *                   DEFAULT: -i
+     * 'sendmail_path' - (string) The location of the sendmail binary on the
+     *                   filesystem.
+     *                   DEFAULT: /usr/sbin/sendmail
+     * </pre>
+     */
+    public function __construct(array $params = array())
+    {
+        if (isset($params['sendmail_args'])) {
+            $this->_sendmailArgs = $params['sendmail_args'];
+        }
+
+        if (isset($params['sendmail_path'])) {
+            $this->_sendmailPath = $params['sendmail_path'];
+        }
+
+        /* Because we need to pass message headers to the sendmail program on
+         * the commandline, we can't guarantee the use of the standard "\r\n"
+         * separator.  Instead, we use the system's native line separator. */
+        $this->sep = defined('PHP_EOL')
+            ? PHP_EOL
+            : (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n";
+    }
+
+    /**
+     * Send a message.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    public function send($recipients, array $headers, $body)
+    {
+        $recipients = implode(' ', array_map('escapeshellarg', $this->parseRecipients($recipients)));
+
+        $headers = $this->_sanitizeHeaders($headers);
+        list($from, $text_headers) = $this->prepareHeaders($headers);
+
+        /* Since few MTAs are going to allow this header to be forged
+         * unless it's in the MAIL FROM: exchange, we'll use Return-Path
+         * instead of From: if it's set. */
+        foreach (array_keys($headers) as $hdr) {
+            if (strcasecmp($hdr, 'Return-Path') === 0) {
+                $from = $headers[$hdr];
+                break;
+            }
+        }
+
+        if (!strlen($from)) {
+            throw new Horde_Mail_Exception('No From address given.');
+        } elseif ((strpos($from, ' ') !== false) ||
+                  (strpos($from, ';') !== false) ||
+                  (strpos($from, '&') !== false) ||
+                  (strpos($from, '`') !== false)) {
+            throw new Horde_Mail_Exception('From address specified with dangerous characters.');
+        }
+
+        $mail = @popen($this->_sendmailPath . (empty($this->_sendmailArgs) ? '' : ' ' . $this->_sendmailargs) . ' -f' . escapeshellarg($from) . ' -- ' . $recipients, 'w');
+        if (!$mail) {
+            throw new Horde_Mail_Exception('Failed to open sendmail [' . $this->_sendmailPath . '] for execution.');
+        }
+
+        // Write the headers following by two newlines: one to end the headers
+        // section and a second to separate the headers block from the body.
+        fputs($mail, $text_headers . $this->sep . $this->sep);
+
+        if (is_resource($body)) {
+            rewind($body);
+            while (!feof($body)) {
+                fputs($mail, fread($body, 8192));
+            }
+        } else {
+            fputs($mail, $body);
+        }
+        $result = pclose($mail);
+
+        if (!$result) {
+            return;
+        }
+
+        switch ($result) {
+        case 64: // EX_USAGE
+            $msg = 'command line usage error';
+            break;
+
+        case 65: // EX_DATAERR
+            $msg =  'data format error';
+            break;
+
+        case 66: // EX_NOINPUT
+            $msg = 'cannot open input';
+            break;
+
+        case 67: // EX_NOUSER
+            $msg = 'addressee unknown';
+            break;
+
+        case 68: // EX_NOHOST
+            $msg = 'host name unknown';
+            break;
+
+        case 69: // EX_UNAVAILABLE
+            $msg = 'service unavailable';
+            break;
+
+        case 70: // EX_SOFTWARE
+            $msg = 'internal software error';
+            break;
+
+        case 71: // EX_OSERR
+            $msg = 'system error';
+            break;
+
+        case 72: // EX_OSFILE
+            $msg = 'critical system file missing';
+            break;
+
+        case 73: // EX_CANTCREAT
+            $msg = 'cannot create output file';
+            break;
+
+        case 74: // EX_IOERR
+            $msg = 'input/output error';
+
+        case 75: // EX_TEMPFAIL
+            $msg = 'temporary failure';
+            break;
+
+        case 76: // EX_PROTOCOL
+            $msg = 'remote error in protocol';
+            break;
+
+        case 77: // EX_NOPERM
+            $msg = 'permission denied';
+            break;
+
+        case 77: // EX_NOPERM
+            $msg = 'permission denied';
+            break;
+
+        case 78: // EX_CONFIG
+            $msg = 'configuration error';
+            break;
+
+        case 79: // EX_NOTFOUND
+            $msg = 'entry not found';
+            break;
+
+        default:
+            $msg = 'unknown error';
+            break;
+        }
+
+        throw new Horde_Mail_Exception('sendmail: ' . $msg . ' (' . $result . ')', $result);
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Smtp.php b/framework/Mail/lib/Horde/Mail/Smtp.php
new file mode 100644 (file)
index 0000000..58b0d83
--- /dev/null
@@ -0,0 +1,356 @@
+<?php
+/**
+ * SMTP implementation.
+ * Requires the Net_SMTP class.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2010, Chuck Hagenbuch
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category  Horde
+ * @package   Mail
+ * @author    Jon Parise <jon@php.net>
+ * @author    Chuck Hagenbuch <chuck@horde.org>
+ * @copyright 2010 Chuck Hagenbuch
+ * @license   http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * SMTP implementation.
+ *
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Smtp extends Horde_Mail_Driver
+{
+    /* Error: Failed to create a Net_SMTP object */
+    const ERROR_CREATE = 10000;
+
+    /* Error: Failed to connect to SMTP server */
+    const ERROR_CONNECT = 10001;
+
+    /* Error: SMTP authentication failure */
+    const ERROR_AUTH = 10002;
+
+    /* Error: No From: address has been provided */
+    const ERROR_FROM = 10003;
+
+    /* Error: Failed to set sender */
+    const ERROR_SENDER = 10004;
+
+    /* Error: Failed to add recipient */
+    const ERROR_RECIPIENT = 10005;
+
+    /* Error: Failed to send data */
+    const ERROR_DATA = 10006;
+
+    /**
+     * The SMTP greeting.
+     *
+     * @var string
+     */
+    public $greeting = null;
+
+    /**
+     * The SMTP queued response.
+     *
+     * @var string
+     */
+    public $queuedAs = null;
+
+    /**
+     * SMTP connection object.
+     *
+     * @var Net_SMTP
+     */
+    protected $_smtp = null;
+
+    /**
+     * The list of service extension parameters to pass to the Net_SMTP
+     * mailFrom() command.
+     *
+     * @var array
+     */
+    protected $_extparams = array();
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  Additional parameters:
+     * <pre>
+     * 'auth' - (mixed) SMTP authentication.
+     *          This value may be set to true, false or the name of a specific
+     *          authentication method.
+     *          If the value is set to true, the Net_SMTP package will attempt
+     *          to use the best authentication method advertised by the remote
+     *          SMTP server.
+     *          DEFAULT: false.
+     * 'debug' - (boolean) Activate SMTP debug mode?
+     *           DEFAULT: false
+     * 'host' - (string) The server to connect to.
+     *          DEFAULT: localhost
+     * 'localhost' - (string) Hostname or domain that will be sent to the
+     *               remote SMTP server in the HELO / EHLO message.
+     *               DEFAULT: localhost
+     * 'password' - (string) The password to use for SMTP auth.
+     *              DEFAULT: NONE
+     * 'persist' - (boolean) Should the SMTP connection persist?
+     *             DEFAULT: false
+     * 'pipelining' - (boolean) Use SMTP command pipelining.
+     *                Use SMTP command pipelining (specified in RFC 2920) if
+     *                the SMTP server supports it. This speeds up delivery
+     *                over high-latency connections.
+     *                DEFAULT: false (use default value from Net_SMTP)
+     * 'port' - (integer) The port to connect to.
+     *          DEFAULT: 25
+     * 'timeout' - (integer) The SMTP connection timeout.
+     *             DEFAULT: NONE
+     * 'username' - (string) The username to use for SMTP auth.
+     *              DEFAULT: NONE
+     * </pre>
+     */
+    public function __construct(array $params = array())
+    {
+        $this->_params = array_merge(array(
+            'auth' => false,
+            'debug' => false,
+            'host' => 'localhost',
+            'localhost' => 'localhost',
+            'password' => '',
+            'persist' => false,
+            'pipelining' => false,
+            'port' => 25,
+            'timeout' => null,
+            'username' => ''
+        ), $params);
+
+        /* Destructor implementation to ensure that we disconnect from any
+         * potentially-alive persistent SMTP connections. */
+        register_shutdown_function(array($this, 'disconnect'));
+    }
+
+    /**
+     * Send a message.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    public function send($recipients, array $headers, $body)
+    {
+        /* If we don't already have an SMTP object, create one. */
+        $this->getSMTPObject();
+
+        $headers = $this->_sanitizeHeaders($headers);
+
+        try {
+            list($from, $textHeaders) = $this->prepareHeaders($headers);
+        } catch (Horde_Mail_Exception $e) {
+            $this->_smtp->rset();
+            throw $e;
+        }
+
+        /* Since few MTAs are going to allow this header to be forged unless
+         * it's in the MAIL FROM: exchange, we'll use Return-Path instead of
+         * From: if it's set. */
+        foreach (array_keys($headers) as $hdr) {
+            if (strcasecmp($hdr, 'Return-Path') === 0) {
+                $from = $headers[$hdr];
+                break;
+            }
+        }
+
+        if (!strlen($from)) {
+            $this->_smtp->rset();
+            throw new Horde_Mail_Exception('No From: address has been provided', self::ERROR_FROM);
+        }
+
+        $params = '';
+        foreach ($this->_extparams as $key => $val) {
+            $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val);
+        }
+
+        $res = $this->_smtp->mailFrom($from, ltrim($params));
+        if ($res instanceof PEAR_Error) {
+            $this->_smtp->rset();
+            $this->_error("Failed to set sender: $from", $res, self::ERROR_SENDER);
+        }
+
+        try {
+            $recipients = $this->parseRecipients($recipients);
+        } catch (Horde_Mail_Exception $e) {
+            $this->_smtp->rset();
+            throw $e;
+        }
+
+        foreach ($recipients as $recipient) {
+            $res = $this->_smtp->rcptTo($recipient);
+            if ($res instanceof PEAR_Error) {
+                $this->_smtp->rset();
+                $this->_error("Failed to add recipient: $recipient", $res, self::ERROR_RECIPIENT);
+            }
+        }
+
+        /* Send the message's headers and the body as SMTP data. */
+        $res = $this->_smtp->data($body, $textHeaders);
+               list(,$args) = $this->_smtp->getResponse();
+
+               if (preg_match("/Ok: queued as (.*)/", $args, $queued)) {
+                       $this->queuedAs = $queued[1];
+               }
+
+        /* We need the greeting; from it we can extract the authorative name
+         * of the mail server we've really connected to. Ideal if we're
+         * connecting to a round-robin of relay servers and need to track
+         * which exact one took the email */
+               $this->greeting = $this->_smtp->getGreeting();
+
+        if ($res instanceof PEAR_Error) {
+            $this->_smtp->rset();
+            $this->_error('Failed to send data', $res, self::ERROR_DATA);
+        }
+
+        /* If persistent connections are disabled, destroy our SMTP object. */
+        if ($this->_params['persist']) {
+            $this->disconnect();
+        }
+    }
+
+    /**
+     * Connect to the SMTP server by instantiating a Net_SMTP object.
+     *
+     * @return Net_SMTP  The SMTP object.
+     * @throws Horde_Mail_Exception
+     */
+    public function getSMTPObject()
+    {
+        if ($this->_smtp) {
+            return $this->_smtp;
+        }
+
+        $this->_smtp = new Net_SMTP(
+            $this->_params['host'],
+            $this->_params['port'],
+            $this->_params['localhost']
+        );
+
+        /* If we still don't have an SMTP object at this point, fail. */
+        if (!($this->_smtp instanceof Net_SMTP)) {
+            throw new Horde_Mail_Exception('Failed to create a Net_SMTP object', self::ERROR_CREATE);
+        }
+
+        /* Configure the SMTP connection. */
+        if ($this->_params['debug']) {
+            $this->_smtp->setDebug(true);
+        }
+
+        /* Attempt to connect to the configured SMTP server. */
+        $res = $this->_smtp->connect($this->_params['timeout']);
+        if ($res instanceof PEAR_Error) {
+            $this->_error('Failed to connect to ' . $this->_params['host'] . ':' . $this->_params['port'], $res, self::ERROR_CONNECT);
+        }
+
+        /* Attempt to authenticate if authentication has been enabled. */
+        if ($this->_params['auth']) {
+            $method = is_string($this->_params['auth'])
+                ? $this->_params['auth']
+                : '';
+
+            $res = $this->_smtp->auth($this->_params['username'], $this->_params['password'], $method);
+            if ($res instanceof PEAR_Error) {
+                $this->_smtp->rset();
+                $this->_error("$method authentication failure", $res, self::ERROR_AUTH);
+            }
+        }
+
+        return $this->_smtp;
+    }
+
+    /**
+     * Add parameter associated with a SMTP service extension.
+     *
+     * @param string $keyword  Extension keyword.
+     * @param string $value    Any value the keyword needs.
+     */
+    public function addServiceExtensionParameter($keyword, $value = null)
+    {
+        $this->_extparams[$keyword] = $value;
+    }
+
+    /**
+     * Disconnect and destroy the current SMTP connection.
+     *
+     * @return boolean True if the SMTP connection no longer exists.
+     */
+    public function disconnect()
+    {
+        /* If we have an SMTP object, disconnect and destroy it. */
+        if (is_object($this->_smtp) && $this->_smtp->disconnect()) {
+            $this->_smtp = null;
+        }
+
+        /* We are disconnected if we no longer have an SMTP object. */
+        return ($this->_smtp === null);
+    }
+
+    /**
+     * Build a standardized string describing the current SMTP error.
+     *
+     * @param string $text       Custom string describing the error context.
+     * @param PEAR_Error $error  PEAR_Error object.
+     * @param integer $e_code    Error code.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    protected function _error($text, $error, $e_code)
+    {
+        /* Split the SMTP response into a code and a response string. */
+        list($code, $response) = $this->_smtp->getResponse();
+
+        /* Build our standardized error string. */
+        throw new Horde_Mail_Exception($text . ' [SMTP: ' . $error->getMessage() . " (code: $code, response: $response)]", $e_code);
+    }
+
+}
diff --git a/framework/Mail/lib/Horde/Mail/Smtpmx.php b/framework/Mail/lib/Horde/Mail/Smtpmx.php
new file mode 100644 (file)
index 0000000..45966a9
--- /dev/null
@@ -0,0 +1,389 @@
+<?PHP
+/**
+ * SMTP MX implementation.
+ * Requires the Net_SMTP class.
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2010, gERD Schaufelberger
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category   Horde
+ * @package    Mail
+ * @author     gERD Schaufelberger <gerd@php-tools.net>
+ * @copyright  2010 gERD Schaufelberger
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
+ */
+
+/**
+ * SMTP MX implementation.
+ *
+ * @author   gERD Schaufelberger <gerd@php-tools.net>
+ * @category Horde
+ * @package  Mail
+ */
+class Horde_Mail_Smtpmx extends Horde_Mail_Driver
+{
+    /**
+     * SMTP connection object.
+     *
+     * @var Net_SMTP
+     */
+    protected $_smtp = null;
+
+    /**
+     * Net_DNS_Resolver object.
+     *
+     * @var Net_DNS_Resolver
+     */
+    protected $_resolver;
+
+    /**
+     * Internal error codes.
+     * Translate internal error identifier to human readable messages.
+     *
+     * @var array
+     */
+    protected $_errorCode = array(
+        'not_connected' => array(
+            'code' => 1,
+            'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.'
+        ),
+        'failed_vrfy_rcpt' => array(
+            'code' => 2,
+            'msg' => 'Recipient "{RCPT}" could not be veryfied.'
+        ),
+        'failed_set_from' => array(
+            'code' => 3,
+            'msg' => 'Failed to set sender: {FROM}.'
+        ),
+        'failed_set_rcpt' => array(
+            'code' => 4,
+            'msg' => 'Failed to set recipient: {RCPT}.'
+        ),
+        'failed_send_data' => array(
+            'code' => 5,
+            'msg' => 'Failed to send mail to: {RCPT}.'
+        ),
+        'no_from' => array(
+            'code' => 5,
+            'msg' => 'No from address has be provided.'
+        ),
+        'send_data' => array(
+            'code' => 7,
+            'msg' => 'Failed to create Net_SMTP object.'
+        ),
+        'no_mx' => array(
+            'code' => 8,
+            'msg' => 'No MX-record for {RCPT} found.'
+        ),
+        'no_resolver' => array(
+            'code' => 9,
+            'msg' => 'Could not start resolver! Install PEAR:Net_DNS or switch off "netdns"'
+        ),
+        'failed_rset' => array(
+            'code' => 10,
+            'msg' => 'RSET command failed, SMTP-connection corrupt.'
+        )
+    );
+
+    /**
+     * Constructor.
+     *
+     * @param array $params  Additional options:
+     * <pre>
+     * 'debug' - (boolean) Activate SMTP and Net_DNS debug mode?
+     *           DEFAULT: false
+     * 'mailname' - (string) The name of the local mail system (a valid
+     *              hostname which matches the reverse lookup)
+     *              DEFAULT: Auto-determined
+     * 'netdns' - (boolean) Use PEAR:Net_DNS (true) or the PHP builtin
+     *            getmxrr().
+     *            DEFAULT: true
+     * 'port' - (integer) Port.
+     *          DEFAULT: Auto-determined
+     * 'test' - (boolean) Activate test mode?
+     *          DEFAULT: false
+     * 'timeout' - (integer) The SMTP connection timeout (in seconds).
+     *             DEFAULT: 10
+     * 'verp' - (boolean) Whether to use VERP.
+     *          If not a boolean, the string value will be used as the VERP
+     *          separators.
+     *          DEFAULT: false
+     * 'vrfy' - (boolean) Whether to use VRFY.
+     *          DEFAULT: false
+     * </pre>
+     */
+    public function __construct(array $params = array())
+    {
+        /* Try to find a valid mailname. */
+        if (!isset($params['mailname']) && function_exists('posix_uname')) {
+            $uname = posix_uname();
+            $params['mailname'] = $uname['nodename'];
+        }
+
+        if (!isset($params['port'])) {
+            $params['port'] = getservbyname('smtp', 'tcp');
+        }
+
+        $this->_params = array_merge(array(
+            'debug' => false,
+            'mailname' => 'localhost',
+            'netdns' => true,
+            'port' => 25,
+            'test' => false,
+            'timeout' => 10,
+            'verp' => false,
+            'vrfy' => false
+        ), $params);
+    }
+
+    /**
+     * Destructor implementation to ensure that we disconnect from any
+     * potentially-alive persistent SMTP connections.
+     */
+    public function __destruct()
+    {
+        if (is_object($this->_smtp)) {
+            $this->_smtp->disconnect();
+            $this->_smtp = null;
+        }
+    }
+
+    /**
+     * Send a message.
+     *
+     * @param mixed $recipients  Either a comma-seperated list of recipients
+     *                           (RFC822 compliant), or an array of
+     *                           recipients, each RFC822 valid. This may
+     *                           contain recipients not specified in the
+     *                           headers, for Bcc:, resending messages, etc.
+     * @param array $headers     The headers to send with the mail, in an
+     *                           associative array, where the array key is the
+     *                           header name (ie, 'Subject'), and the array
+     *                           value is the header value (ie, 'test'). The
+     *                           header produced from those values would be
+     *                           'Subject: test'.
+     *                           If the '_raw' key exists, the value of this
+     *                           key will be used as the exact text for
+     *                           sending the message.
+     * @param mixed $body        The full text of the message body, including
+     *                           any Mime parts, etc. Either a string or a
+     *                           stream resource.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    public function send($recipients, array $headers, $body)
+    {
+        $headers = $this->_sanitizeHeaders($headers);
+
+        // Prepare headers
+        list($from, $textHeaders) = $this->prepareHeaders($headers);
+
+        // Use 'Return-Path' if possible
+        foreach (array_keys($headers) as $hdr) {
+            if (strcasecmp($hdr, 'Return-Path') === 0) {
+                $from = $headers['Return-Path'];
+                break;
+            }
+        }
+
+        if (!strlen($from)) {
+            $this->_error('no_from');
+        }
+
+        // Prepare recipients
+        foreach ($this->parseRecipients($recipients) as $rcpt) {
+            list($user, $host) = explode('@', $rcpt);
+
+            $mx = $this->_getMx($host);
+            if (!$mx) {
+                $this->_error('no_mx', array('rcpt' => $rcpt));
+            }
+
+            $connected = false;
+            foreach ($mx as $mserver => $mpriority) {
+                $this->_smtp = new Net_SMTP($mserver, $this->_params['port'], $this->_params['mailname']);
+
+                // configure the SMTP connection.
+                if ($this->_params['debug']) {
+                    $this->_smtp->setDebug(true);
+                }
+
+                // attempt to connect to the configured SMTP server.
+                $res = $this->_smtp->connect($this->_params['timeout']);
+                if ($res instanceof PEAR_Error) {
+                    $this->_smtp = null;
+                    continue;
+                }
+
+                // connection established
+                if ($res) {
+                    $connected = true;
+                    break;
+                }
+            }
+
+            if (!$connected) {
+                $this->_error('not_connected', array(
+                    'host' => implode(', ', array_keys($mx)),
+                    'port' => $this->_params['port'],
+                    'rcpt' => $rcpt
+                ));
+            }
+
+            // Verify recipient
+            if ($this->_params['vrfy']) {
+                $res = $this->_smtp->vrfy($rcpt);
+                if ($res instanceof PEAR_Error) {
+                    $this->_error('failed_vrfy_rcpt', array('rcpt' => $rcpt));
+                }
+            }
+
+            // mail from:
+            $args['verp'] = $this->_params['verp'];
+            $res = $this->_smtp->mailFrom($from, $args);
+            if ($res instanceof PEAR_Error) {
+                $this->_error('failed_set_from', array('from' => $from));
+            }
+
+            // rcpt to:
+            $res = $this->_smtp->rcptTo($rcpt);
+            if ($res instanceof PEAR_Error) {
+                $this->_error('failed_set_rcpt', array('rcpt' => $rcpt));
+            }
+
+            // Don't send anything in test mode
+            if ($this->_params['test']) {
+                $res = $this->_smtp->rset();
+                if ($res instanceof PEAR_Error) {
+                    $this->_error('failed_rset');
+                }
+
+                $this->_smtp->disconnect();
+                $this->_smtp = null;
+                return;
+            }
+
+            // Send data
+            $res = $this->_smtp->data($body, $textHeaders);
+            if ($res instanceof PEAR_Error) {
+                $this->_error('failed_send_data', array('rcpt' => $rcpt));
+            }
+
+            $this->_smtp->disconnect();
+            $this->_smtp = null;
+        }
+    }
+
+    /**
+     * Recieve MX records for a host.
+     *
+     * @param string $host  Mail host.
+     *
+     * @return mixed  Sorted MX list or false on error.
+     */
+    protected function _getMx($host)
+    {
+        $mx = array();
+
+        if ($this->params['netdns']) {
+            $this->_loadNetDns();
+
+            $response = $this->_resolver->query($host, 'MX');
+            if (!$response) {
+                return false;
+            }
+
+            foreach ($response->answer as $rr) {
+                if ($rr->type == 'MX') {
+                    $mx[$rr->exchange] = $rr->preference;
+                }
+            }
+        } else {
+            $mxHost = $mxWeight = array();
+
+            if (!getmxrr($host, $mxHost, $mxWeight)) {
+                return false;
+            }
+
+            for ($i = 0; $i < count($mxHost); ++$i) {
+                $mx[$mxHost[$i]] = $mxWeight[$i];
+            }
+        }
+
+        asort($mx);
+
+        return $mx;
+    }
+
+    /**
+     * Initialize Net_DNS_Resolver.
+     */
+    protected function _loadNetDns()
+    {
+        if (!$this->_resolver) {
+            if (!class_exists('Net_DNS_Resolver')) {
+                $this->_error('no_resolver');
+            }
+
+            $this->_resolver = new Net_DNS_Resolver();
+            if ($this->_params['debug']) {
+                $this->_resolver->test = 1;
+            }
+        }
+    }
+
+    /**
+     * Format error message.
+     *
+     * @param string $id   Maps error ids to codes and message.
+     * @param array $info  Optional information in associative array.
+     *
+     * @throws Horde_Mail_Exception
+     */
+    protected function _error($id, $info = array())
+    {
+        $msg = $this->_errorCode[$id]['msg'];
+
+        // include info to messages
+        if (!empty($info)) {
+            $replace = $search = array();
+
+            foreach ($info as $key => $value) {
+                $search[] = '{' . strtoupper($key) . '}';
+                $replace[] = $value;
+            }
+
+            $msg = str_replace($search, $replace, $msg);
+        }
+
+        throw new Horde_Mail_Exception($msg, $this->_errorCode[$id]['code']);
+    }
+
+}
diff --git a/framework/Mail/package.xml b/framework/Mail/package.xml
new file mode 100644 (file)
index 0000000..835cd21
--- /dev/null
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Mail</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Mail Library</summary>
+ <description>The Horde_Mail:: library is a fork of the PEAR Mail library that provides additional functionality, including (but not limited to):
+ * Allows a stream to be passed in.
+ * Allows raw headertext to be used in the outgoing messages (required for
+   things like message redirection pursuant to RFC 5322 [3.6.6]).
+ * Native PHP 5 code.
+ * PHPUnit test suite.
+ * Provides more comprehensive sendmail error messages.
+ * Uses Exceptions instead of PEAR_Errors.
+ </description>
+ <lead>
+  <name>Michael Slusarz</name>
+  <user>slusarz</user>
+  <email>slusarz@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2010-05-11</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>* Initial Horde Release.
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Mail">
+      <file name="Driver.php" role="php" />
+      <file name="Exception.php" role="php" />
+      <file name="Mail.php" role="php" />
+      <file name="Mock.php" role="php" />
+      <file name="Null.php" role="php" />
+      <file name="Rfc822.php" role="php" />
+      <file name="Sendmail.php" role="php" />
+      <file name="Smtp.php" role="php" />
+      <file name="Smtpmx.php" role="php" />
+     </dir> <!-- /lib/Horde/Mail -->
+     <file name="Mail.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Mail">
+      <file name="AllTests.php" role="test" />
+      <file name="ParseTest.php" role="test" />
+     </dir> <!-- /test/Horde/Mail -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+   <package>
+    <name>Exception</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <package>
+    <name>Net_SMTP</name>
+    <channel>pear.php.net</channel>
+    <min>1.4.0</min>
+   </package>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Mail/Driver.php" as="Horde/Mail/Driver.php" />
+   <install name="lib/Horde/Mail/Exception.php" as="Horde/Mail/Exception.php" />
+   <install name="lib/Horde/Mail/Mail.php" as="Horde/Mail/Mail.php" />
+   <install name="lib/Horde/Mail/Mock.php" as="Horde/Mail/Mock.php" />
+   <install name="lib/Horde/Mail/Null.php" as="Horde/Mail/Null.php" />
+   <install name="lib/Horde/Mail/Rfc822.php" as="Horde/Mail/Rfc822.php" />
+   <install name="lib/Horde/Mail/Sendmail.php" as="Horde/Mail/Sendmail.php" />
+   <install name="lib/Horde/Mail/Smtp.php" as="Horde/Mail/Smtp.php" />
+   <install name="lib/Horde/Mail/Smtpmx.php" as="Horde/Mail/Smtpmx.php" />
+   <install name="lib/Horde/Mail.php" as="Horde/Mail.php" />
+   <install name="test/Horde/Mail/AllTests.php" as="test/Horde/Mail/AllTests.php" />
+   <install name="test/Horde/Mail/ParseTest.php" as="test/Horde/Mail/ParseTest.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Mail/test/Horde/Mail/AllTests.php b/framework/Mail/test/Horde/Mail/AllTests.php
new file mode 100644 (file)
index 0000000..4b1be36
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Horde_Mail test suite
+ *
+ * @author     Michael Slusarz <slusarz@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Mail
+ * @subpackage UnitTests
+ */
+
+/**
+ * Define the main method
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Mail_AllTests::main');
+}
+
+/**
+ * Prepare the test setup.
+ */
+require_once 'Horde/Test/AllTests.php';
+
+/**
+ * @package    Mail
+ * @subpackage UnitTests
+ */
+class Horde_Mail_AllTests extends Horde_Test_AllTests
+{
+}
+
+Horde_Mail_AllTests::init('Horde_Mail', __FILE__);
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Mail_AllTests::main') {
+    Horde_Mail_AllTests::main();
+}
diff --git a/framework/Mail/test/Horde/Mail/ParseTest.php b/framework/Mail/test/Horde/Mail/ParseTest.php
new file mode 100644 (file)
index 0000000..f095c1c
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+/**
+ * @author     Michael Slusarz <slusarz@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Mail
+ * @subpackage UnitTests
+ */
+
+class Horde_Mail_ParseTest extends PHPUnit_Framework_TestCase
+{
+    /* Test case for PEAR Mail:: bug #13659 */
+    public function testParseBug13659()
+    {
+        $address = '"Test Student" <test@mydomain.com> (test)';
+
+        $parser = new Horde_Mail_Rfc822();
+        $result = $parser->parseAddressList($address, array(
+           'default_domain' => 'anydomain.com'
+        ));
+
+        $this->assertTrue(is_array($result) &&
+            is_object($result[0]) &&
+            ($result[0]->personal == '"Test Student"') &&
+            ($result[0]->mailbox == "test") &&
+            ($result[0]->host == "mydomain.com") &&
+            is_array($result[0]->comment) &&
+            ($result[0]->comment[0] == 'test'));
+    }
+
+    /* Test case for PEAR Mail:: bug #9137 */
+    public function testParseBug9137()
+    {
+        $addresses = array(
+            array('name' => 'John Doe', 'email' => 'test@example.com'),
+            array('name' => 'John Doe\\', 'email' => 'test@example.com'),
+            array('name' => 'John "Doe', 'email' => 'test@example.com'),
+            array('name' => 'John "Doe\\', 'email' => 'test@example.com'),
+        );
+
+        $parser = new Horde_Mail_Rfc822();
+
+        foreach ($addresses as $val) {
+            $address =
+                '"' . addslashes($val['name']) . '" <' . $val['email'] . '>';
+
+            /* Throws Exception on error. */
+            $parser->parseAddressList($address);
+        }
+    }
+
+    /* Test case for PEAR Mail:: bug #9137, take 2 */
+    public function testParseBug9137Take2()
+    {
+        $addresses = array(
+            array(
+                'raw' => '"John Doe" <test@example.com>'
+            ),
+            array(
+                'raw' => '"John Doe' . chr(92) . '" <test@example.com>',
+                'fail' => true
+            ),
+            array(
+                'raw' => '"John Doe' . chr(92) . chr(92) . '" <test@example.com>'
+            ),
+            array(
+                'raw' => '"John Doe' . chr(92) . chr(92) . chr(92) . '" <test@example.com>',
+                'fail' => true
+            ),
+            array(
+                'raw' => '"John Doe' . chr(92) . chr(92) . chr(92) . chr(92) . '" <test@example.com>'
+            ),
+            array(
+                'raw' => '"John Doe <test@example.com>',
+                'fail' => true
+            )
+        );
+
+        $parser = new Horde_Mail_Rfc822();
+
+        foreach ($addresses as $val) {
+            try {
+                $parser->parseAddressList($val['raw']);
+                if (!empty($val['fail'])) {
+                    $this->fail('An expected exception was not raised.');
+                }
+            } catch (Horde_Mail_Exception $e) {
+                if (empty($val['fail'])) {
+                    $this->fail('An unexpected exception was raised.');
+                }
+            }
+        }
+    }
+
+    public function testGeneralParsing()
+    {
+        $parser = new Horde_Mail_Rfc822();
+
+        /* A simple, bare address. */
+        $address = 'user@example.com';
+        $result = $parser->parseAddressList($address, array(
+            'default_domain' => null
+        ));
+
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result);
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_OBJECT, $result[0]);
+        $this->assertEquals($result[0]->personal, '');
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result[0]->comment);
+        $this->assertEquals($result[0]->comment, array());
+        $this->assertEquals($result[0]->mailbox, 'user');
+        $this->assertEquals($result[0]->host, 'example.com');
+
+        /* Address groups. */
+        $address = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;';
+        $result = $parser->parseAddressList($address, array(
+            'default_domain' => null
+        ));
+
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result);
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_OBJECT, $result[0]);
+        $this->assertEquals($result[0]->groupname, 'My Group');
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result[0]->addresses);
+
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_OBJECT, $result[0]->addresses[0]);
+        $this->assertEquals($result[0]->addresses[0]->personal, '"Richard"');
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result[0]->addresses[0]->comment);
+        $this->assertEquals($result[0]->addresses[0]->comment[0], 'A comment');
+        $this->assertEquals($result[0]->addresses[0]->mailbox, 'richard');
+        $this->assertEquals($result[0]->addresses[0]->host, 'localhost');
+
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_OBJECT, $result[0]->addresses[1]);
+        $this->assertEquals($result[0]->addresses[1]->personal, '');
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result[0]->addresses[1]->comment);
+        $this->assertEquals($result[0]->addresses[1]->comment[0], 'Ted Bloggs');
+        $this->assertEquals($result[0]->addresses[1]->mailbox, 'ted');
+        $this->assertEquals($result[0]->addresses[1]->host, 'example.com');
+
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_OBJECT, $result[0]->addresses[2]);
+        $this->assertEquals($result[0]->addresses[2]->personal, '');
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result[0]->addresses[2]->comment);
+        $this->assertEquals($result[0]->addresses[2]->comment, array());
+        $this->assertEquals($result[0]->addresses[2]->mailbox, 'Barney');
+        $this->assertEquals($result[0]->addresses[2]->host, 'localhost');
+
+        /* A valid address with spaces in the local part. */
+        $address = '<"Jon Parise"@php.net>';
+        $result = $parser->parseAddressList($address, array(
+            'default_domain' => null
+        ));
+
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result);
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_OBJECT, $result[0]);
+        $this->assertEquals($result[0]->personal, '');
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $result[0]->comment);
+        $this->assertEquals($result[0]->comment, array());
+        $this->assertEquals($result[0]->mailbox, '"Jon Parise"');
+        $this->assertEquals($result[0]->host, 'php.net');
+
+        /* An invalid address with spaces in the local part. */
+        $address = '<Jon Parise@php.net>';
+        try {
+            $parser->parseAddressList($address, array(
+                'default_domain' => null
+            ));
+            $this->fail('An expected exception was not raised.');
+        } catch (Horde_Mail_Exception $e) {}
+
+        /* A valid address with an uncommon TLD. */
+        $address = 'jon@host.longtld';
+        try {
+            $parser->parseAddressList($address, array(
+                'default_domain' => null
+            ));
+        } catch (Horde_Mail_Exception $e) {
+            $this->fail('An unexpected exception was raised.');
+        }
+    }
+
+    public function testValidateQuotedString()
+    {
+        $address_string = '"Joe Doe \(from Somewhere\)" <doe@example.com>, postmaster@example.com, root';
+
+        $parser = new Horde_Mail_Rfc822();
+
+        $res = $parser->parseAddressList($address_string, array(
+            'default_domain' => 'example.com'
+        ));
+        $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $res);
+        $this->assertEquals(count($res), 3);
+    }
+
+}