From 86a3e6e87180f613fe5654fdf0db9f85c159d41b Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Fri, 6 Jan 2017 22:05:31 +0000 Subject: [PATCH] Mail: Upgrade PHPMailer to 5.2.21. Merges [39645], [36083] to the 4.3 branch. See #37210. Built from https://develop.svn.wordpress.org/branches/4.3@39725 git-svn-id: http://core.svn.wordpress.org/branches/4.3@39665 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/class-phpmailer.php | 840 +++++++++++++++++++++++--------- wp-includes/class-smtp.php | 172 +++++-- 2 files changed, 750 insertions(+), 262 deletions(-) diff --git a/wp-includes/class-phpmailer.php b/wp-includes/class-phpmailer.php index 338965ff4b..f83697d92b 100644 --- a/wp-includes/class-phpmailer.php +++ b/wp-includes/class-phpmailer.php @@ -29,65 +29,66 @@ class PHPMailer { /** * The PHPMailer Version number. - * @type string + * @var string */ - public $Version = '5.2.10'; + public $Version = '5.2.21'; /** * Email priority. - * Options: 1 = High, 3 = Normal, 5 = low. - * @type integer + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * @var integer */ - public $Priority = 3; + public $Priority = null; /** * The character set of the message. - * @type string + * @var string */ public $CharSet = 'iso-8859-1'; /** * The MIME Content-type of the message. - * @type string + * @var string */ public $ContentType = 'text/plain'; /** * The message encoding. * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". - * @type string + * @var string */ public $Encoding = '8bit'; /** * Holds the most recent mailer error message. - * @type string + * @var string */ public $ErrorInfo = ''; /** * The From email address for the message. - * @type string + * @var string */ public $From = 'root@localhost'; /** * The From name of the message. - * @type string + * @var string */ public $FromName = 'Root User'; /** * The Sender email (Return-Path) of the message. * If not empty, will be sent via -f to sendmail or as 'MAIL FROM' in smtp mode. - * @type string + * @var string */ public $Sender = ''; /** * The Return-Path of the message. * If empty, it will be set to either From or Sender. - * @type string + * @var string * @deprecated Email senders should never set a return-path header; * it's the receiver's job (RFC5321 section 4.4), so this no longer does anything. * @link https://tools.ietf.org/html/rfc5321#section-4.4 RFC5321 reference @@ -96,14 +97,14 @@ class PHPMailer /** * The Subject of the message. - * @type string + * @var string */ public $Subject = ''; /** * An HTML or plain text message body. * If HTML then call isHTML(true). - * @type string + * @var string */ public $Body = ''; @@ -112,7 +113,7 @@ class PHPMailer * This body can be read by mail clients that do not have HTML email * capability such as mutt & Eudora. * Clients that can read HTML will view the normal Body. - * @type string + * @var string */ public $AltBody = ''; @@ -122,27 +123,27 @@ class PHPMailer * To generate iCal events, use the bundled extras/EasyPeasyICS.php class or iCalcreator * @link http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ * @link http://kigkonsult.se/iCalcreator/ - * @type string + * @var string */ public $Ical = ''; /** * The complete compiled MIME message body. * @access protected - * @type string + * @var string */ protected $MIMEBody = ''; /** * The complete compiled MIME message headers. - * @type string + * @var string * @access protected */ protected $MIMEHeader = ''; /** * Extra headers that createHeader() doesn't fold in. - * @type string + * @var string * @access protected */ protected $mailHeader = ''; @@ -150,64 +151,67 @@ class PHPMailer /** * Word-wrap the message body to this number of chars. * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. - * @type integer + * @var integer */ public $WordWrap = 0; /** * Which method to use to send mail. * Options: "mail", "sendmail", or "smtp". - * @type string + * @var string */ public $Mailer = 'mail'; /** * The path to the sendmail program. - * @type string + * @var string */ public $Sendmail = '/usr/sbin/sendmail'; /** * Whether mail() uses a fully sendmail-compatible MTA. * One which supports sendmail's "-oi -f" options. - * @type boolean + * @var boolean */ public $UseSendmailOptions = true; /** * Path to PHPMailer plugins. * Useful if the SMTP class is not in the PHP include path. - * @type string + * @var string * @deprecated Should not be needed now there is an autoloader. */ public $PluginDir = ''; /** - * The email address that a reading confirmation should be sent to. - * @type string + * The email address that a reading confirmation should be sent to, also known as read receipt. + * @var string */ public $ConfirmReadingTo = ''; /** - * The hostname to use in Message-Id and Received headers - * and as default HELO string. - * If empty, the value returned - * by SERVER_NAME is used or 'localhost.localdomain'. - * @type string + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * @var string */ public $Hostname = ''; /** - * An ID to be used in the Message-Id header. + * An ID to be used in the Message-ID header. * If empty, a unique id will be generated. - * @type string + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 + * @var string */ public $MessageID = ''; /** * The message Date to be used in the Date header. * If empty, the current date will be added. - * @type string + * @var string */ public $MessageDate = ''; @@ -220,21 +224,22 @@ class PHPMailer * You can also specify encryption type, for example: * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). * Hosts will be tried in order. - * @type string + * @var string */ public $Host = 'localhost'; /** * The default SMTP server port. - * @type integer + * @var integer * @TODO Why is this needed when the SMTP class takes care of it? */ public $Port = 25; /** * The SMTP HELO of the message. - * Default is $Hostname. - * @type string + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * @var string * @see PHPMailer::$Hostname */ public $Helo = ''; @@ -242,7 +247,7 @@ class PHPMailer /** * What kind of encryption to use on the SMTP connection. * Options: '', 'ssl' or 'tls' - * @type string + * @var string */ public $SMTPSecure = ''; @@ -250,14 +255,14 @@ class PHPMailer * Whether to enable TLS encryption automatically if a server supports it, * even if `SMTPSecure` is not set to 'tls'. * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. - * @type boolean + * @var boolean */ public $SMTPAutoTLS = true; /** * Whether to use SMTP authentication. * Uses the Username and Password properties. - * @type boolean + * @var boolean * @see PHPMailer::$Username * @see PHPMailer::$Password */ @@ -265,47 +270,47 @@ class PHPMailer /** * Options array passed to stream_context_create when connecting via SMTP. - * @type array + * @var array */ public $SMTPOptions = array(); /** * SMTP username. - * @type string + * @var string */ public $Username = ''; /** * SMTP password. - * @type string + * @var string */ public $Password = ''; /** * SMTP auth type. - * Options are LOGIN (default), PLAIN, NTLM, CRAM-MD5 - * @type string + * Options are CRAM-MD5, LOGIN, PLAIN, attempted in that order if not specified + * @var string */ public $AuthType = ''; /** * SMTP realm. * Used for NTLM auth - * @type string + * @var string */ public $Realm = ''; /** * SMTP workstation. * Used for NTLM auth - * @type string + * @var string */ public $Workstation = ''; /** * The SMTP server timeout in seconds. * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 - * @type integer + * @var integer */ public $Timeout = 300; @@ -318,7 +323,7 @@ class PHPMailer * * `2` Data and commands * * `3` As 2 plus connection status * * `4` Low-level data output - * @type integer + * @var integer * @see SMTP::$do_debug */ public $SMTPDebug = 0; @@ -334,7 +339,7 @@ class PHPMailer * * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; * - * @type string|callable + * @var string|callable * @see SMTP::$Debugoutput */ public $Debugoutput = 'echo'; @@ -343,20 +348,21 @@ class PHPMailer * Whether to keep SMTP connection open after each message. * If this is set to true then to close the connection * requires an explicit call to smtpClose(). - * @type boolean + * @var boolean */ public $SMTPKeepAlive = false; /** * Whether to split multiple to addresses into multiple messages * or send them all in one message. - * @type boolean + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * @var boolean */ public $SingleTo = false; /** * Storage for addresses when SingleTo is enabled. - * @type array + * @var array * @TODO This should really not be public */ public $SingleToArray = array(); @@ -364,15 +370,15 @@ class PHPMailer /** * Whether to generate VERP addresses on send. * Only applicable when sending via SMTP. - * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link https://en.wikipedia.org/wiki/Variable_envelope_return_path * @link http://www.postfix.org/VERP_README.html Postfix VERP info - * @type boolean + * @var boolean */ public $do_verp = false; /** * Whether to allow sending messages with an empty body. - * @type boolean + * @var boolean */ public $AllowEmpty = false; @@ -380,43 +386,50 @@ class PHPMailer * The default line ending. * @note The default remains "\n". We force CRLF where we know * it must be used via self::CRLF. - * @type string + * @var string */ public $LE = "\n"; /** * DKIM selector. - * @type string + * @var string */ public $DKIM_selector = ''; /** * DKIM Identity. - * Usually the email address used as the source of the email - * @type string + * Usually the email address used as the source of the email. + * @var string */ public $DKIM_identity = ''; /** * DKIM passphrase. * Used if your key is encrypted. - * @type string + * @var string */ public $DKIM_passphrase = ''; /** * DKIM signing domain name. * @example 'example.com' - * @type string + * @var string */ public $DKIM_domain = ''; /** * DKIM private key file path. - * @type string + * @var string */ public $DKIM_private = ''; + /** + * DKIM private key string. + * If set, takes precedence over `$DKIM_private`. + * @var string + */ + public $DKIM_private_string = ''; + /** * Callback Action function name. * @@ -433,48 +446,57 @@ class PHPMailer * string $subject the subject * string $body the email body * string $from email address of sender - * @type string + * @var string */ public $action_function = ''; /** * What to put in the X-Mailer header. * Options: An empty string for PHPMailer default, whitespace for none, or a string to use - * @type string + * @var string */ public $XMailer = ''; + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * @see PHPMailer::validateAddress() + * @var string|callable + * @static + */ + public static $validator = 'auto'; + /** * An instance of the SMTP sender class. - * @type SMTP + * @var SMTP * @access protected */ protected $smtp = null; /** - * The array of 'to' addresses. - * @type array + * The array of 'to' names and addresses. + * @var array * @access protected */ protected $to = array(); /** - * The array of 'cc' addresses. - * @type array + * The array of 'cc' names and addresses. + * @var array * @access protected */ protected $cc = array(); /** - * The array of 'bcc' addresses. - * @type array + * The array of 'bcc' names and addresses. + * @var array * @access protected */ protected $bcc = array(); /** * The array of reply-to names and addresses. - * @type array + * @var array * @access protected */ protected $ReplyTo = array(); @@ -482,77 +504,100 @@ class PHPMailer /** * An array of all kinds of addresses. * Includes all of $to, $cc, $bcc - * @type array + * @var array * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc */ protected $all_recipients = array(); + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + */ + protected $RecipientsQueue = array(); + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$ReplyTo + */ + protected $ReplyToQueue = array(); + /** * The array of attachments. - * @type array + * @var array * @access protected */ protected $attachment = array(); /** * The array of custom headers. - * @type array + * @var array * @access protected */ protected $CustomHeader = array(); /** * The most recent Message-ID (including angular brackets). - * @type string + * @var string * @access protected */ protected $lastMessageID = ''; /** * The message's MIME type. - * @type string + * @var string * @access protected */ protected $message_type = ''; /** * The array of MIME boundary strings. - * @type array + * @var array * @access protected */ protected $boundary = array(); /** * The array of available languages. - * @type array + * @var array * @access protected */ protected $language = array(); /** * The number of errors encountered. - * @type integer + * @var integer * @access protected */ protected $error_count = 0; /** * The S/MIME certificate file path. - * @type string + * @var string * @access protected */ protected $sign_cert_file = ''; /** * The S/MIME key file path. - * @type string + * @var string * @access protected */ protected $sign_key_file = ''; /** * The optional S/MIME extra certificates ("CA Chain") file path. - * @type string + * @var string * @access protected */ protected $sign_extracerts_file = ''; @@ -560,21 +605,21 @@ class PHPMailer /** * The S/MIME password for the key. * Used only if the key is encrypted. - * @type string + * @var string * @access protected */ protected $sign_key_pass = ''; /** * Whether to throw exceptions for errors. - * @type boolean + * @var boolean * @access protected */ protected $exceptions = false; /** * Unique ID used for message ID and boundaries. - * @type string + * @var string * @access protected */ protected $uniqueid = ''; @@ -601,7 +646,7 @@ class PHPMailer /** * The maximum line length allowed by RFC 2822 section 2.1.1 - * @type integer + * @var integer */ const MAX_LINE_LENGTH = 998; @@ -609,9 +654,11 @@ class PHPMailer * Constructor. * @param boolean $exceptions Should we throw external exceptions? */ - public function __construct($exceptions = false) + public function __construct($exceptions = null) { - $this->exceptions = (boolean)$exceptions; + if ($exceptions !== null) { + $this->exceptions = (boolean)$exceptions; + } } /** @@ -620,9 +667,7 @@ class PHPMailer public function __destruct() { //Close any open SMTP connection nicely - if ($this->Mailer == 'smtp') { - $this->smtpClose(); - } + $this->smtpClose(); } /** @@ -646,14 +691,16 @@ class PHPMailer } else { $subject = $this->encodeHeader($this->secureHeader($subject)); } - if (ini_get('safe_mode') || !($this->UseSendmailOptions)) { + + //Can't use additional_parameters in safe_mode, calling mail() with null params breaks + //@link http://php.net/manual/en/function.mail.php + if (ini_get('safe_mode') or !$this->UseSendmailOptions or is_null($params)) { $result = @mail($to, $subject, $body, $header); } else { $result = @mail($to, $subject, $body, $header, $params); } return $result; } - /** * Output debugging info via user-defined method. * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). @@ -688,7 +735,7 @@ class PHPMailer case 'echo': default: //Normalize line breaks - $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str); + $str = preg_replace('/\r\n?/ms', "\n", $str); echo gmdate('Y-m-d H:i:s') . "\t" . str_replace( "\n", "\n \t ", @@ -763,55 +810,101 @@ class PHPMailer /** * Add a "To" address. - * @param string $address + * @param string $address The email address to send to * @param string $name - * @return boolean true on success, false if address already used + * @return boolean true on success, false if address already used or invalid in some way */ public function addAddress($address, $name = '') { - return $this->addAnAddress('to', $address, $name); + return $this->addOrEnqueueAnAddress('to', $address, $name); } /** * Add a "CC" address. * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. - * @param string $address + * @param string $address The email address to send to * @param string $name - * @return boolean true on success, false if address already used + * @return boolean true on success, false if address already used or invalid in some way */ public function addCC($address, $name = '') { - return $this->addAnAddress('cc', $address, $name); + return $this->addOrEnqueueAnAddress('cc', $address, $name); } /** * Add a "BCC" address. * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. - * @param string $address + * @param string $address The email address to send to * @param string $name - * @return boolean true on success, false if address already used + * @return boolean true on success, false if address already used or invalid in some way */ public function addBCC($address, $name = '') { - return $this->addAnAddress('bcc', $address, $name); + return $this->addOrEnqueueAnAddress('bcc', $address, $name); } /** - * Add a "Reply-to" address. - * @param string $address + * Add a "Reply-To" address. + * @param string $address The email address to reply to * @param string $name - * @return boolean + * @return boolean true on success, false if address already used or invalid in some way */ public function addReplyTo($address, $name = '') { - return $this->addAnAddress('Reply-To', $address, $name); + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); } /** - * Add an address to one of the recipient arrays. - * Addresses that have been added already return false, but do not throw exceptions - * @param string $kind One of 'to', 'cc', 'bcc', 'ReplyTo' - * @param string $address The email address to send to + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * @throws phpmailerException + * @return boolean true on success, false if address already used or invalid in some way + * @access protected + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + if (($pos = strrpos($address, '@')) === false) { + // At-sign is misssing. + $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address"; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + $params = array($kind, $address, $name); + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if ($this->has8bitChars(substr($address, ++$pos)) and $this->idnSupported()) { + if ($kind != 'Reply-To') { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + return true; + } + } else { + if (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + return true; + } + } + return false; + } + // Immediately add standard addresses without IDN. + return call_user_func_array(array($this, 'addAnAddress'), $params); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to * @param string $name * @throws phpmailerException * @return boolean true on success, false if address already used or invalid in some way @@ -819,26 +912,26 @@ class PHPMailer */ protected function addAnAddress($kind, $address, $name = '') { - if (!preg_match('/^(to|cc|bcc|Reply-To)$/', $kind)) { - $this->setError($this->lang('Invalid recipient array') . ': ' . $kind); - $this->edebug($this->lang('Invalid recipient array') . ': ' . $kind); + if (!in_array($kind, array('to', 'cc', 'bcc', 'Reply-To'))) { + $error_message = $this->lang('Invalid recipient kind: ') . $kind; + $this->setError($error_message); + $this->edebug($error_message); if ($this->exceptions) { - throw new phpmailerException('Invalid recipient array: ' . $kind); + throw new phpmailerException($error_message); } return false; } - $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim if (!$this->validateAddress($address)) { - $this->setError($this->lang('invalid_address') . ': ' . $address); - $this->edebug($this->lang('invalid_address') . ': ' . $address); + $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address"; + $this->setError($error_message); + $this->edebug($error_message); if ($this->exceptions) { - throw new phpmailerException($this->lang('invalid_address') . ': ' . $address); + throw new phpmailerException($error_message); } return false; } if ($kind != 'Reply-To') { - if (!isset($this->all_recipients[strtolower($address)])) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { array_push($this->$kind, array($address, $name)); $this->all_recipients[strtolower($address)] = true; return true; @@ -852,6 +945,61 @@ class PHPMailer return false; } + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * @return array + * @link http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + */ + public function parseAddresses($addrstr, $useimap = true) + { + $addresses = array(); + if ($useimap and function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if ($address->host != '.SYNTAX-ERROR.') { + if ($this->validateAddress($address->mailbox . '@' . $address->host)) { + $addresses[] = array( + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host + ); + } + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if ($this->validateAddress($address)) { + $addresses[] = array( + 'name' => '', + 'address' => $address + ); + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + if ($this->validateAddress($email)) { + $addresses[] = array( + 'name' => trim(str_replace(array('"', "'"), '', $name)), + 'address' => $email + ); + } + } + } + } + return $addresses; + } + /** * Set the From and FromName properties. * @param string $address @@ -864,11 +1012,15 @@ class PHPMailer { $address = trim($address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim - if (!$this->validateAddress($address)) { - $this->setError($this->lang('invalid_address') . ': ' . $address); - $this->edebug($this->lang('invalid_address') . ': ' . $address); + // Don't validate now addresses with IDN. Will be done in send(). + if (($pos = strrpos($address, '@')) === false or + (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and + !$this->validateAddress($address)) { + $error_message = $this->lang('invalid_address') . " (setFrom) $address"; + $this->setError($error_message); + $this->edebug($error_message); if ($this->exceptions) { - throw new phpmailerException($this->lang('invalid_address') . ': ' . $address); + throw new phpmailerException($error_message); } return false; } @@ -897,19 +1049,34 @@ class PHPMailer /** * Check that a string looks like an email address. * @param string $address The email address to check - * @param string $patternselect A selector for the validation pattern to use : - * * `auto` Pick strictest one automatically; + * @param string|callable $patternselect A selector for the validation pattern to use : + * * `auto` Pick best pattern automatically; * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14; * * `pcre` Use old PCRE implementation; - * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; same as pcre8 but does not allow 'dotless' domains; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. * @return boolean * @static * @access public */ - public static function validateAddress($address, $patternselect = 'auto') + public static function validateAddress($address, $patternselect = null) { + if (is_null($patternselect)) { + $patternselect = self::$validator; + } + if (is_callable($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) { + return false; + } if (!$patternselect or $patternselect == 'auto') { //Check this constant first so it works when extension_loaded() is disabled by safe mode //Constant was added in PHP 5.2.4 @@ -989,6 +1156,48 @@ class PHPMailer } } + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * "intl" and "mbstring" PHP extensions. + * @return bool "true" if required functions for IDN support are present + */ + public function idnSupported() + { + // @TODO: Write our own "idn_to_ascii" function for PHP <= 5.2. + return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain has characters not allowed in an IDN) + * @see PHPMailer::$CharSet + * @param string $address The email address to convert + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + // Verify we have required functions, CharSet, and at-sign. + if ($this->idnSupported() and + !empty($this->CharSet) and + ($pos = strrpos($address, '@')) !== false) { + $domain = substr($address, ++$pos); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) { + $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); + if (($punycode = defined('INTL_IDNA_VARIANT_UTS46') ? + idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46) : + idn_to_ascii($domain)) !== false) { + return substr($address, 0, $pos) . $punycode; + } + } + } + return $address; + } + /** * Create a message and send it. * Uses the sending method specified by $Mailer. @@ -1020,17 +1229,41 @@ class PHPMailer public function preSend() { try { + $this->error_count = 0; // Reset errors $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array(array($this, 'addAnAddress'), $params); + } if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) { throw new phpmailerException($this->lang('provide_address'), self::STOP_CRITICAL); } + // Validate From, Sender, and ConfirmReadingTo addresses + foreach (array('From', 'Sender', 'ConfirmReadingTo') as $address_kind) { + $this->$address_kind = trim($this->$address_kind); + if (empty($this->$address_kind)) { + continue; + } + $this->$address_kind = $this->punyencodeAddress($this->$address_kind); + if (!$this->validateAddress($this->$address_kind)) { + $error_message = $this->lang('invalid_address') . ' (punyEncode) ' . $this->$address_kind; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + } + // Set whether the message is multipart/alternative - if (!empty($this->AltBody)) { + if ($this->alternativeExists()) { $this->ContentType = 'multipart/alternative'; } - $this->error_count = 0; // Reset errors $this->setMessageType(); // Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty and empty($this->Body)) { @@ -1061,9 +1294,11 @@ class PHPMailer // Sign with DKIM if enabled if (!empty($this->DKIM_domain) - && !empty($this->DKIM_private) && !empty($this->DKIM_selector) - && file_exists($this->DKIM_private)) { + && (!empty($this->DKIM_private_string) + || (!empty($this->DKIM_private) && file_exists($this->DKIM_private)) + ) + ) { $header_dkim = $this->DKIM_Add( $this->MIMEHeader . $this->mailHeader, $this->encodeHeader($this->secureHeader($this->Subject)), @@ -1129,19 +1364,24 @@ class PHPMailer */ protected function sendmailSend($header, $body) { - if ($this->Sender != '') { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) and self::isShellSafe($this->Sender)) { if ($this->Mailer == 'qmail') { - $sendmail = sprintf('%s -f%s', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + $sendmailFmt = '%s -f%s'; } else { - $sendmail = sprintf('%s -oi -f%s -t', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + $sendmailFmt = '%s -oi -f%s -t'; } } else { if ($this->Mailer == 'qmail') { - $sendmail = sprintf('%s', escapeshellcmd($this->Sendmail)); + $sendmailFmt = '%s'; } else { - $sendmail = sprintf('%s -oi -t', escapeshellcmd($this->Sendmail)); + $sendmailFmt = '%s -oi -t'; } } + + // TODO: If possible, this should be changed to escapeshellarg. Needs thorough testing. + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + if ($this->SingleTo) { foreach ($this->SingleToArray as $toAddr) { if (!@$mail = popen($sendmail, 'w')) { @@ -1171,7 +1411,15 @@ class PHPMailer fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); - $this->doCallback(($result == 0), $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $this->doCallback( + ($result == 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); if ($result != 0) { throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } @@ -1179,6 +1427,40 @@ class PHPMailer return true; } + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * @param string $string The string to be validated + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * @access protected + * @return boolean + */ + protected static function isShellSafe($string) + { + // Future-proof + if (escapeshellcmd($string) !== $string + or !in_array(escapeshellarg($string), array("'$string'", "\"$string\"")) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $c = $string[$i]; + + // All other characters have a special meaning in at least one common shell, including = and +. + // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + // Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + /** * Send mail using the PHP mail() function. * @param string $header The message headers @@ -1196,17 +1478,20 @@ class PHPMailer } $to = implode(', ', $toArr); - if (empty($this->Sender)) { - $params = ' '; - } else { - $params = sprintf('-f%s', $this->Sender); + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + if (!empty($this->Sender) and $this->validateAddress($this->Sender)) { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } } - if ($this->Sender != '' and !ini_get('safe_mode')) { + if (!empty($this->Sender) and !ini_get('safe_mode') and $this->validateAddress($this->Sender)) { $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $this->Sender); } $result = false; - if ($this->SingleTo && count($toArr) > 1) { + if ($this->SingleTo and count($toArr) > 1) { foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); $this->doCallback($result, array($toAddr), $this->cc, $this->bcc, $this->Subject, $body, $this->From); @@ -1256,10 +1541,10 @@ class PHPMailer if (!$this->smtpConnect($this->SMTPOptions)) { throw new phpmailerException($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); } - if ('' == $this->Sender) { - $smtp_from = $this->From; - } else { + if (!empty($this->Sender) and $this->validateAddress($this->Sender)) { $smtp_from = $this->Sender; + } else { + $smtp_from = $this->From; } if (!$this->smtp->mail($smtp_from)) { $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); @@ -1313,12 +1598,17 @@ class PHPMailer * @throws phpmailerException * @return boolean */ - public function smtpConnect($options = array()) + public function smtpConnect($options = null) { if (is_null($this->smtp)) { $this->smtp = $this->getSMTPInstance(); } + //If no options are provided, use whatever is set in the instance + if (is_null($options)) { + $options = $this->SMTPOptions; + } + // Already connected? if ($this->smtp->connected()) { return true; @@ -1388,7 +1678,7 @@ class PHPMailer if (!$this->smtp->startTLS()) { throw new phpmailerException($this->lang('connect_host')); } - // We must resend HELO after tls negotiation + // We must resend EHLO after TLS negotiation $this->smtp->hello($hello); } if ($this->SMTPAuth) { @@ -1427,7 +1717,7 @@ class PHPMailer */ public function smtpClose() { - if ($this->smtp !== null) { + if (is_a($this->smtp, 'SMTP')) { if ($this->smtp->connected()) { $this->smtp->quit(); $this->smtp->close(); @@ -1446,6 +1736,19 @@ class PHPMailer */ public function setLanguage($langcode = 'en', $lang_path = '') { + // Backwards compatibility for renamed language codes + $renamed_langcodes = array( + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + ); + + if (isset($renamed_langcodes[$langcode])) { + $langcode = $renamed_langcodes[$langcode]; + } + // Define full set of translatable strings in English $PHPMAILER_LANG = array( 'authenticate' => 'SMTP Error: Could not authenticate.', @@ -1458,7 +1761,7 @@ class PHPMailer 'file_open' => 'File Error: Could not open file: ', 'from_failed' => 'The following From address failed: ', 'instantiate' => 'Could not instantiate mail function.', - 'invalid_address' => 'Invalid address', + 'invalid_address' => 'Invalid address: ', 'mailer_not_supported' => ' mailer is not supported.', 'provide_address' => 'You must provide at least one recipient email address.', 'recipients_failed' => 'SMTP Error: The following recipients failed: ', @@ -1472,6 +1775,10 @@ class PHPMailer // Calculate an absolute path so it can work if CWD is not here $lang_path = dirname(__FILE__). DIRECTORY_SEPARATOR . 'language'. DIRECTORY_SEPARATOR; } + //Validate $langcode + if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $langcode = 'en'; + } $foundlang = true; $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; // There is no English translation file @@ -1723,7 +2030,6 @@ class PHPMailer } $result .= $this->headerLine('Date', $this->MessageDate); - // To be created automatically by mail() if ($this->SingleTo) { if ($this->Mailer != 'mail') { @@ -1766,17 +2072,21 @@ class PHPMailer $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); } - if ($this->MessageID != '') { + // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) { $this->lastMessageID = $this->MessageID; } else { - $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->ServerHostname()); + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); } $result .= $this->headerLine('Message-ID', $this->lastMessageID); - $result .= $this->headerLine('X-Priority', $this->Priority); + if (!is_null($this->Priority)) { + $result .= $this->headerLine('X-Priority', $this->Priority); + } if ($this->XMailer == '') { $result .= $this->headerLine( 'X-Mailer', - 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer/)' + 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer)' ); } else { $myXmailer = trim($this->XMailer); @@ -1786,7 +2096,7 @@ class PHPMailer } if ($this->ConfirmReadingTo != '') { - $result .= $this->headerLine('Disposition-Notification-To', '<' . trim($this->ConfirmReadingTo) . '>'); + $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); } // Add custom headers @@ -1866,7 +2176,15 @@ class PHPMailer */ public function getSentMIMEMessage() { - return $this->MIMEHeader . $this->mailHeader . self::CRLF . $this->MIMEBody; + return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . self::CRLF . self::CRLF . $this->MIMEBody; + } + + /** + * Create unique ID + * @return string + */ + protected function generateId() { + return md5(uniqid(time())); } /** @@ -1880,7 +2198,7 @@ class PHPMailer { $body = ''; //Create unique IDs and preset boundaries - $this->uniqueid = md5(uniqid(time())); + $this->uniqueid = $this->generateId(); $this->boundary[1] = 'b1_' . $this->uniqueid; $this->boundary[2] = 'b2_' . $this->uniqueid; $this->boundary[3] = 'b3_' . $this->uniqueid; @@ -1896,11 +2214,12 @@ class PHPMailer //Can we do a 7-bit downgrade? if ($bodyEncoding == '8bit' and !$this->has8bitChars($this->Body)) { $bodyEncoding = '7bit'; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit $bodyCharSet = 'us-ascii'; } - //If lines are too long, change to quoted-printable transfer encoding - if (self::hasLineLongerThanMax($this->Body)) { - $this->Encoding = 'quoted-printable'; + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the body part only + if ('base64' != $this->Encoding and self::hasLineLongerThanMax($this->Body)) { $bodyEncoding = 'quoted-printable'; } @@ -1909,10 +2228,12 @@ class PHPMailer //Can we do a 7-bit downgrade? if ($altBodyEncoding == '8bit' and !$this->has8bitChars($this->AltBody)) { $altBodyEncoding = '7bit'; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit $altBodyCharSet = 'us-ascii'; } - //If lines are too long, change to quoted-printable transfer encoding - if (self::hasLineLongerThanMax($this->AltBody)) { + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if ('base64' != $altBodyEncoding and self::hasLineLongerThanMax($this->AltBody)) { $altBodyEncoding = 'quoted-printable'; } //Use this as a preamble in all multipart message types @@ -2015,8 +2336,10 @@ class PHPMailer $body .= $this->attachAll('attachment', $this->boundary[1]); break; default: - // catch case 'plain' and case '' - $body .= $this->encodeString($this->Body, $bodyEncoding); + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + //Reset the `Encoding` property in case we changed it for line length reasons + $this->Encoding = $bodyEncoding; + $body .= $this->encodeString($this->Body, $this->Encoding); break; } @@ -2122,8 +2445,7 @@ class PHPMailer /** * Set the message type. - * PHPMailer only supports some preset message types, - * not arbitrary MIME structures. + * PHPMailer only supports some preset message types, not arbitrary MIME structures. * @access protected * @return void */ @@ -2141,6 +2463,7 @@ class PHPMailer } $this->message_type = implode('_', $type); if ($this->message_type == '') { + //The 'plain' message_type refers to the message having a single body element, not that it is plain-text $this->message_type = 'plain'; } } @@ -2266,18 +2589,27 @@ class PHPMailer $type = $attachment[4]; $disposition = $attachment[6]; $cid = $attachment[7]; - if ($disposition == 'inline' && isset($cidUniq[$cid])) { + if ($disposition == 'inline' && array_key_exists($cid, $cidUniq)) { continue; } $cidUniq[$cid] = true; $mime[] = sprintf('--%s%s', $boundary, $this->LE); - $mime[] = sprintf( - 'Content-Type: %s; name="%s"%s', - $type, - $this->encodeHeader($this->secureHeader($name)), - $this->LE - ); + //Only include a filename property if we have one + if (!empty($name)) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader($this->secureHeader($name)), + $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + $this->LE + ); + } // RFC1341 part 5 says 7bit is assumed if not specified if ($encoding != '7bit') { $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, $this->LE); @@ -2301,12 +2633,20 @@ class PHPMailer $this->LE . $this->LE ); } else { - $mime[] = sprintf( - 'Content-Disposition: %s; filename=%s%s', - $disposition, - $encoded_name, - $this->LE . $this->LE - ); + if (!empty($encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + $this->LE . $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + $this->LE . $this->LE + ); + } } } else { $mime[] = $this->LE; @@ -2340,7 +2680,6 @@ class PHPMailer * @param string $path The full path to the file * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' * @throws phpmailerException - * @see EncodeFile(encodeFile * @access protected * @return string */ @@ -2560,7 +2899,7 @@ class PHPMailer { // Use native function if it's available (>= PHP5.3) if (function_exists('quoted_printable_encode')) { - return $this->fixEOL(quoted_printable_encode($string)); + return quoted_printable_encode($string); } // Fall back to a pure PHP implementation $string = str_replace( @@ -2568,8 +2907,7 @@ class PHPMailer array(' ', "\r\n=2E", "\r\n", '='), rawurlencode($string) ); - $string = preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string); - return $this->fixEOL($string); + return preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string); } /** @@ -2638,7 +2976,6 @@ class PHPMailer return str_replace(' ', '_', $encoded); } - /** * Add a string or binary attachment (non-filesystem). * This method can be used to attach ascii or binary data, @@ -2744,7 +3081,7 @@ class PHPMailer $disposition = 'inline' ) { // If a MIME type is not specified, try to work it out from the name - if ($type == '') { + if ($type == '' and !empty($name)) { $type = self::filenameToType($name); } @@ -2800,6 +3137,22 @@ class PHPMailer return !empty($this->AltBody); } + /** + * Clear queued addresses of given kind. + * @access protected + * @param string $kind 'to', 'cc', or 'bcc' + * @return void + */ + public function clearQueuedAddresses($kind) + { + $RecipientsQueue = $this->RecipientsQueue; + foreach ($RecipientsQueue as $address => $params) { + if ($params[0] == $kind) { + unset($this->RecipientsQueue[$address]); + } + } + } + /** * Clear all To recipients. * @return void @@ -2810,6 +3163,7 @@ class PHPMailer unset($this->all_recipients[strtolower($to[0])]); } $this->to = array(); + $this->clearQueuedAddresses('to'); } /** @@ -2822,6 +3176,7 @@ class PHPMailer unset($this->all_recipients[strtolower($cc[0])]); } $this->cc = array(); + $this->clearQueuedAddresses('cc'); } /** @@ -2834,6 +3189,7 @@ class PHPMailer unset($this->all_recipients[strtolower($bcc[0])]); } $this->bcc = array(); + $this->clearQueuedAddresses('bcc'); } /** @@ -2843,6 +3199,7 @@ class PHPMailer public function clearReplyTos() { $this->ReplyTo = array(); + $this->ReplyToQueue = array(); } /** @@ -2855,6 +3212,7 @@ class PHPMailer $this->cc = array(); $this->bcc = array(); $this->all_recipients = array(); + $this->RecipientsQueue = array(); } /** @@ -3011,8 +3369,7 @@ class PHPMailer } /** - * Returns all custom headers - * + * Returns all custom headers. * @return array */ public function getCustomHeaders() @@ -3021,21 +3378,23 @@ class PHPMailer } /** - * Create a message from an HTML string. - * Automatically makes modifications for inline images and backgrounds - * and creates a plain-text version by converting the HTML. - * Overwrites any existing values in $this->Body and $this->AltBody + * Create a message body from an HTML string. + * Automatically inlines images and creates a plain-text version by converting the HTML, + * overwriting any existing values in Body and AltBody. + * $basedir is used when handling relative image paths, e.g. + * will look for an image file in $basedir/images/a.png and convert it to inline. + * If you don't want to apply these transformations to your HTML, just set Body and AltBody yourself. * @access public * @param string $message HTML message string - * @param string $basedir baseline directory for path + * @param string $basedir base directory for relative paths to images * @param boolean|callable $advanced Whether to use the internal HTML to text converter - * or your own custom converter @see html2text() - * @return string $message + * or your own custom converter @see PHPMailer::html2text() + * @return string $message The transformed message Body */ public function msgHTML($message, $basedir = '', $advanced = false) { preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); - if (isset($images[2])) { + if (array_key_exists(2, $images)) { foreach ($images[2] as $imgindex => $url) { // Convert data URIs into embedded images if (preg_match('#^data:(image[^;,]*)(;base64)?,#', $url, $match)) { @@ -3046,15 +3405,16 @@ class PHPMailer $data = rawurldecode($data); } $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 - if ($this->addStringEmbeddedImage($data, $cid, '', 'base64', $match[1])) { + if ($this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, 'base64', $match[1])) { $message = str_replace( $images[0][$imgindex], $images[1][$imgindex] . '="cid:' . $cid . '"', $message ); } - } elseif (!preg_match('#^[A-z]+://#', $url)) { + } elseif (substr($url, 0, 4) !== 'cid:' && !preg_match('#^[a-z][a-z0-9+.-]*://#i', $url)) { // Do not change urls for absolute images (thanks to corvuscorax) + // Do not change urls that are already inline images $filename = basename($url); $directory = dirname($url); if ($directory == '.') { @@ -3088,7 +3448,7 @@ class PHPMailer // Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better $this->Body = $this->normalizeBreaks($message); $this->AltBody = $this->normalizeBreaks($this->html2text($message, $advanced)); - if (empty($this->AltBody)) { + if (!$this->alternativeExists()) { $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . self::CRLF . self::CRLF; } @@ -3099,7 +3459,7 @@ class PHPMailer * Convert an HTML string into plain text. * This is used by msgHTML(). * Note - older versions of this function used a bundled advanced converter - * which was been removed for license reasons in #232 + * which was been removed for license reasons in #232. * Example usage: * * // Use default conversion @@ -3144,6 +3504,16 @@ class PHPMailer 'bin' => 'application/macbinary', 'doc' => 'application/msword', 'word' => 'application/msword', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'class' => 'application/octet-stream', 'dll' => 'application/octet-stream', 'dms' => 'application/octet-stream', @@ -3348,7 +3718,6 @@ class PHPMailer return preg_replace('/(\r\n|\r|\n)/ms', $breaktype, $text); } - /** * Set the public and private key files and password for S/MIME signing. * @access public @@ -3390,7 +3759,7 @@ class PHPMailer * @access public * @param string $signHeader * @throws phpmailerException - * @return string + * @return string The DKIM signature value */ public function DKIM_Sign($signHeader) { @@ -3400,15 +3769,35 @@ class PHPMailer } return ''; } - $privKeyStr = file_get_contents($this->DKIM_private); - if ($this->DKIM_passphrase != '') { + $privKeyStr = !empty($this->DKIM_private_string) ? $this->DKIM_private_string : file_get_contents($this->DKIM_private); + if ('' != $this->DKIM_passphrase) { $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); } else { - $privKey = $privKeyStr; + $privKey = openssl_pkey_get_private($privKeyStr); } - if (openssl_sign($signHeader, $signature, $privKey)) { - return base64_encode($signature); + //Workaround for missing digest algorithms in old PHP & OpenSSL versions + //@link http://stackoverflow.com/a/11117338/333340 + if (version_compare(PHP_VERSION, '5.3.0') >= 0 and + in_array('sha256WithRSAEncryption', openssl_get_md_methods(true))) { + if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { + openssl_pkey_free($privKey); + return base64_encode($signature); + } + } else { + $pinfo = openssl_pkey_get_details($privKey); + $hash = hash('sha256', $signHeader); + //'Magic' constant for SHA256 from RFC3447 + //@link https://tools.ietf.org/html/rfc3447#page-43 + $t = '3031300d060960864801650304020105000420' . $hash; + $pslen = $pinfo['bits'] / 8 - (strlen($t) / 2 + 3); + $eb = pack('H*', '0001' . str_repeat('FF', $pslen) . '00' . $t); + + if (openssl_private_encrypt($eb, $signature, $privKey, OPENSSL_NO_PADDING)) { + openssl_pkey_free($privKey); + return base64_encode($signature); + } } + openssl_pkey_free($privKey); return ''; } @@ -3425,7 +3814,7 @@ class PHPMailer foreach ($lines as $key => $line) { list($heading, $value) = explode(':', $line, 2); $heading = strtolower($heading); - $value = preg_replace('/\s+/', ' ', $value); // Compress useless spaces + $value = preg_replace('/\s{2,}/', ' ', $value); // Compress useless spaces $lines[$key] = $heading . ':' . trim($value); // Don't forget to remove WSP around the value } $signHeader = implode("\r\n", $lines); @@ -3463,7 +3852,7 @@ class PHPMailer */ public function DKIM_Add($headers_line, $subject, $body) { - $DKIMsignatureType = 'rsa-sha1'; // Signature & hash algorithms + $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body $DKIMquery = 'dns/txt'; // Query method $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) @@ -3471,6 +3860,7 @@ class PHPMailer $headers = explode($this->LE, $headers_line); $from_header = ''; $to_header = ''; + $date_header = ''; $current = ''; foreach ($headers as $header) { if (strpos($header, 'From:') === 0) { @@ -3479,6 +3869,9 @@ class PHPMailer } elseif (strpos($header, 'To:') === 0) { $to_header = $header; $current = 'to_header'; + } elseif (strpos($header, 'Date:') === 0) { + $date_header = $header; + $current = 'date_header'; } else { if (!empty($$current) && strpos($header, ' =?') === 0) { $$current .= $header; @@ -3489,6 +3882,7 @@ class PHPMailer } $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); + $date = str_replace('|', '=7C', $this->DKIM_QP($date_header)); $subject = str_replace( '|', '=7C', @@ -3496,7 +3890,7 @@ class PHPMailer ); // Copied header fields (dkim-quoted-printable) $body = $this->DKIM_BodyC($body); $DKIMlen = strlen($body); // Length of body - $DKIMb64 = base64_encode(pack('H*', sha1($body))); // Base64 of packed binary SHA-1 hash of body + $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body if ('' == $this->DKIM_identity) { $ident = ''; } else { @@ -3509,15 +3903,20 @@ class PHPMailer $this->DKIM_selector . ";\r\n" . "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . - "\th=From:To:Subject;\r\n" . + "\th=From:To:Date:Subject;\r\n" . "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . "\tz=$from\r\n" . "\t|$to\r\n" . + "\t|$date\r\n" . "\t|$subject;\r\n" . "\tbh=" . $DKIMb64 . ";\r\n" . "\tb="; $toSign = $this->DKIM_HeaderC( - $from_header . "\r\n" . $to_header . "\r\n" . $subject_header . "\r\n" . $dkimhdrs + $from_header . "\r\n" . + $to_header . "\r\n" . + $date_header . "\r\n" . + $subject_header . "\r\n" . + $dkimhdrs ); $signed = $this->DKIM_Sign($toSign); return $dkimhdrs . $signed . "\r\n"; @@ -3537,6 +3936,7 @@ class PHPMailer /** * Allows for public read access to 'to' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. * @access public * @return array */ @@ -3547,6 +3947,7 @@ class PHPMailer /** * Allows for public read access to 'cc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. * @access public * @return array */ @@ -3557,6 +3958,7 @@ class PHPMailer /** * Allows for public read access to 'bcc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. * @access public * @return array */ @@ -3567,6 +3969,7 @@ class PHPMailer /** * Allows for public read access to 'ReplyTo' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. * @access public * @return array */ @@ -3577,6 +3980,7 @@ class PHPMailer /** * Allows for public read access to 'all_recipients' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. * @access public * @return array */ diff --git a/wp-includes/class-smtp.php b/wp-includes/class-smtp.php index 1eae77a128..3aa48f4fb0 100644 --- a/wp-includes/class-smtp.php +++ b/wp-includes/class-smtp.php @@ -28,25 +28,25 @@ class SMTP { /** * The PHPMailer SMTP version number. - * @type string + * @var string */ - const VERSION = '5.2.10'; + const VERSION = '5.2.21'; /** * SMTP line break constant. - * @type string + * @var string */ const CRLF = "\r\n"; /** * The SMTP port to use if one is not specified. - * @type integer + * @var integer */ const DEFAULT_SMTP_PORT = 25; /** * The maximum line length allowed by RFC 2822 section 2.1.1 - * @type integer + * @var integer */ const MAX_LINE_LENGTH = 998; @@ -77,15 +77,15 @@ class SMTP /** * The PHPMailer SMTP Version number. - * @type string + * @var string * @deprecated Use the `VERSION` constant instead * @see SMTP::VERSION */ - public $Version = '5.2.10'; + public $Version = '5.2.21'; /** * SMTP server port number. - * @type integer + * @var integer * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead * @see SMTP::DEFAULT_SMTP_PORT */ @@ -93,7 +93,7 @@ class SMTP /** * SMTP reply line ending. - * @type string + * @var string * @deprecated Use the `CRLF` constant instead * @see SMTP::CRLF */ @@ -107,7 +107,7 @@ class SMTP * * self::DEBUG_SERVER (`2`) Client commands and server responses * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages - * @type integer + * @var integer */ public $do_debug = self::DEBUG_OFF; @@ -122,7 +122,7 @@ class SMTP * * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; * - * @type string|callable + * @var string|callable */ public $Debugoutput = 'echo'; @@ -130,7 +130,7 @@ class SMTP * Whether to use VERP. * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path * @link http://www.postfix.org/VERP_README.html Info on VERP - * @type boolean + * @var boolean */ public $do_verp = false; @@ -139,26 +139,37 @@ class SMTP * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2 - * @type integer + * @var integer */ public $Timeout = 300; /** * How long to wait for commands to complete, in seconds. * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 - * @type integer + * @var integer */ public $Timelimit = 300; + /** + * @var array patterns to extract smtp transaction id from smtp reply + * Only first capture group will be use, use non-capturing group to deal with it + * Extend this class to override this property to fulfil your needs. + */ + protected $smtp_transaction_id_patterns = array( + 'exim' => '/[0-9]{3} OK id=(.*)/', + 'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/', + 'postfix' => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/' + ); + /** * The socket for the server connection. - * @type resource + * @var resource */ protected $smtp_conn; /** * Error information, if any, for the last SMTP command. - * @type array + * @var array */ protected $error = array( 'error' => '', @@ -170,7 +181,7 @@ class SMTP /** * The reply the server sent to us for HELO. * If null, no HELO string has yet been received. - * @type string|null + * @var string|null */ protected $helo_rply = null; @@ -181,13 +192,13 @@ class SMTP * represents the server name. In case of HELO it is the only element of the array. * Other values can be boolean TRUE or an array containing extension options. * If null, no HELO/EHLO string has yet been received. - * @type array|null + * @var array|null */ protected $server_caps = null; /** * The most recent reply received from the server. - * @type string + * @var string */ protected $last_reply = ''; @@ -206,7 +217,7 @@ class SMTP } //Avoid clash with built-in function names if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) { - call_user_func($this->Debugoutput, $str, $this->do_debug); + call_user_func($this->Debugoutput, $str, $level); return; } switch ($this->Debugoutput) { @@ -272,8 +283,8 @@ class SMTP $errstr = ''; if ($streamok) { $socket_context = stream_context_create($options); - //Suppress errors; connection failures are handled at a higher level - $this->smtp_conn = @stream_socket_client( + set_error_handler(array($this, 'errorHandler')); + $this->smtp_conn = stream_socket_client( $host . ":" . $port, $errno, $errstr, @@ -281,12 +292,14 @@ class SMTP STREAM_CLIENT_CONNECT, $socket_context ); + restore_error_handler(); } else { //Fall back to fsockopen which should work in more places, but is missing some features $this->edebug( "Connection: stream_socket_client not available, falling back to fsockopen", self::DEBUG_CONNECTION ); + set_error_handler(array($this, 'errorHandler')); $this->smtp_conn = fsockopen( $host, $port, @@ -294,6 +307,7 @@ class SMTP $errstr, $timeout ); + restore_error_handler(); } // Verify we connected properly if (!is_resource($this->smtp_conn)) { @@ -336,11 +350,22 @@ class SMTP if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { return false; } + + //Allow the best TLS version(s) we can + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT + //so add them back in manually if we can + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + // Begin encrypted connection if (!stream_socket_enable_crypto( $this->smtp_conn, true, - STREAM_CRYPTO_METHOD_TLS_CLIENT + $crypto_method )) { return false; } @@ -351,20 +376,21 @@ class SMTP * Perform SMTP authentication. * Must be run after hello(). * @see hello() - * @param string $username The user name - * @param string $password The password - * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5) - * @param string $realm The auth realm for NTLM + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (PLAIN, LOGIN, CRAM-MD5) + * @param string $realm The auth realm for NTLM * @param string $workstation The auth workstation for NTLM - * @access public - * @return boolean True if successfully authenticated. + * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth) + * @return bool True if successfully authenticated.* @access public */ public function authenticate( $username, $password, $authtype = null, $realm = '', - $workstation = '' + $workstation = '', + $OAuth = null ) { if (!$this->server_caps) { $this->setError('Authentication is not allowed before HELO/EHLO'); @@ -388,7 +414,7 @@ class SMTP ); if (empty($authtype)) { - foreach (array('LOGIN', 'CRAM-MD5', 'PLAIN') as $method) { + foreach (array('CRAM-MD5', 'LOGIN', 'PLAIN') as $method) { if (in_array($method, $this->server_caps['AUTH'])) { $authtype = $method; break; @@ -672,10 +698,12 @@ class SMTP protected function parseHelloFields($type) { $this->server_caps = array(); - $lines = explode("\n", $this->last_reply); + $lines = explode("\n", $this->helo_rply); + foreach ($lines as $n => $s) { + //First 4 chars contain response code followed by - or space $s = trim(substr($s, 4)); - if (!$s) { + if (empty($s)) { continue; } $fields = explode(' ', $s); @@ -685,11 +713,20 @@ class SMTP $fields = $fields[0]; } else { $name = array_shift($fields); - if ($name == 'SIZE') { - $fields = ($fields) ? $fields[0] : 0; + switch ($name) { + case 'SIZE': + $fields = ($fields ? $fields[0] : 0); + break; + case 'AUTH': + if (!is_array($fields)) { + $fields = array(); + } + break; + default: + $fields = true; } } - $this->server_caps[$name] = ($fields ? $fields : true); + $this->server_caps[$name] = $fields; } } } @@ -739,15 +776,15 @@ class SMTP * Sets the TO argument to $toaddr. * Returns true if the recipient was accepted false if it was rejected. * Implements from rfc 821: RCPT TO: - * @param string $toaddr The address the message is being sent to + * @param string $address The address the message is being sent to * @access public * @return boolean */ - public function recipient($toaddr) + public function recipient($address) { return $this->sendCommand( 'RCPT TO', - 'RCPT TO:<' . $toaddr . '>', + 'RCPT TO:<' . $address . '>', array(250, 251) ); } @@ -766,9 +803,9 @@ class SMTP /** * Send a command to an SMTP server and check its return code. - * @param string $command The command name - not sent to the server + * @param string $command The command name - not sent to the server * @param string $commandstring The actual command to send - * @param integer|array $expect One or more expected integer success codes + * @param integer|array $expect One or more expected integer success codes * @access protected * @return boolean True on success. */ @@ -778,6 +815,11 @@ class SMTP $this->setError("Called $command without being connected"); return false; } + //Reject line breaks in all commands + if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) { + $this->setError("Command '$command' contained line breaks"); + return false; + } $this->client_send($commandstring . self::CRLF); $this->last_reply = $this->get_lines(); @@ -981,10 +1023,9 @@ class SMTP } while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { $str = @fgets($this->smtp_conn, 515); - $this->edebug("SMTP -> get_lines(): \$data was \"$data\"", self::DEBUG_LOWLEVEL); - $this->edebug("SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL); - $data .= $str; $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL); + $this->edebug("SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL); + $data .= $str; // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen if ((isset($str[3]) and $str[3] == ' ')) { break; @@ -1099,4 +1140,47 @@ class SMTP { return $this->Timeout; } + + /** + * Reports an error number and string. + * @param integer $errno The error number returned by PHP. + * @param string $errmsg The error message returned by PHP. + */ + protected function errorHandler($errno, $errmsg) + { + $notice = 'Connection: Failed to connect to server.'; + $this->setError( + $notice, + $errno, + $errmsg + ); + $this->edebug( + $notice . ' Error number ' . $errno . '. "Error notice: ' . $errmsg, + self::DEBUG_CONNECTION + ); + } + + /** + * Will return the ID of the last smtp transaction based on a list of patterns provided + * in SMTP::$smtp_transaction_id_patterns. + * If no reply has been received yet, it will return null. + * If no pattern has been matched, it will return false. + * @return bool|null|string + */ + public function getLastTransactionID() + { + $reply = $this->getLastReply(); + + if (empty($reply)) { + return null; + } + + foreach($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) { + if(preg_match($smtp_transaction_id_pattern, $reply, $matches)) { + return $matches[1]; + } + } + + return false; + } }