--- /dev/null
+<?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);
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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
+{
+}
--- /dev/null
+<?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.');
+ }
+ }
+
+}
--- /dev/null
+<?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));
+ }
+ }
+
+}
--- /dev/null
+<?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)
+ {
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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);
+ }
+
+}
--- /dev/null
+<?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);
+ }
+
+}
--- /dev/null
+<?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']);
+ }
+
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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();
+}
--- /dev/null
+<?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);
+ }
+
+}