X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/f5fcdc7994bb67cce809bc4777944ae8b7fad4a4..refs/tags/wordpress-4.3:/wp-includes/class-phpmailer.php diff --git a/wp-includes/class-phpmailer.php b/wp-includes/class-phpmailer.php index c38632ab..338965ff 100644 --- a/wp-includes/class-phpmailer.php +++ b/wp-includes/class-phpmailer.php @@ -1,15 +1,14 @@ + * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) * @author Brent R. Matzelle (original founder) - * @copyright 2013 Marcus Bointon + * @copyright 2012 - 2014 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License @@ -18,21 +17,13 @@ * FITNESS FOR A PARTICULAR PURPOSE. */ -if (version_compare(PHP_VERSION, '5.0.0', '<')) { - exit("Sorry, PHPMailer will only run on PHP version 5 or greater!\n"); -} - /** * PHPMailer - PHP email creation and transport class. - * PHP Version 5.0.0 * @package PHPMailer - * @author Marcus Bointon (coolbru) + * @author Marcus Bointon (Synchro/coolbru) * @author Jim Jagielski (jimjag) * @author Andy Prevost (codeworxtech) * @author Brent R. Matzelle (original founder) - * @copyright 2013 Marcus Bointon - * @copyright 2010 - 2012 Jim Jagielski - * @copyright 2004 - 2009 Andy Prevost */ class PHPMailer { @@ -40,12 +31,12 @@ class PHPMailer * The PHPMailer Version number. * @type string */ - public $Version = '5.2.7'; + public $Version = '5.2.10'; /** * Email priority. * Options: 1 = High, 3 = Normal, 5 = low. - * @type int + * @type integer */ public $Priority = 3; @@ -97,6 +88,9 @@ class PHPMailer * The Return-Path of the message. * If empty, it will be set to either From or Sender. * @type 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 */ public $ReturnPath = ''; @@ -155,7 +149,8 @@ class PHPMailer /** * Word-wrap the message body to this number of chars. - * @type int + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * @type integer */ public $WordWrap = 0; @@ -175,7 +170,7 @@ class PHPMailer /** * Whether mail() uses a fully sendmail-compatible MTA. * One which supports sendmail's "-oi -f" options. - * @type bool + * @type boolean */ public $UseSendmailOptions = true; @@ -222,6 +217,8 @@ class PHPMailer * You can also specify a different port * for each host by using this format: [hostname:port] * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * 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 */ @@ -229,8 +226,8 @@ class PHPMailer /** * The default SMTP server port. - * @type int - * @Todo Why is this needed when the SMTP class takes care of it? + * @type integer + * @TODO Why is this needed when the SMTP class takes care of it? */ public $Port = 25; @@ -243,21 +240,35 @@ class PHPMailer public $Helo = ''; /** - * The secure connection prefix. - * Options: "", "ssl" or "tls" + * What kind of encryption to use on the SMTP connection. + * Options: '', 'ssl' or 'tls' * @type string */ public $SMTPSecure = ''; + /** + * 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 + */ + public $SMTPAutoTLS = true; + /** * Whether to use SMTP authentication. * Uses the Username and Password properties. - * @type bool + * @type boolean * @see PHPMailer::$Username * @see PHPMailer::$Password */ public $SMTPAuth = false; + /** + * Options array passed to stream_context_create when connecting via SMTP. + * @type array + */ + public $SMTPOptions = array(); + /** * SMTP username. * @type string @@ -293,45 +304,60 @@ class PHPMailer /** * The SMTP server timeout in seconds. - * @type int + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @type integer */ - public $Timeout = 10; + public $Timeout = 300; /** * SMTP class debug output mode. - * Options: 0 = off, 1 = commands, 2 = commands and data - * @type int + * Debug output level. + * Options: + * * `0` No output + * * `1` Commands + * * `2` Data and commands + * * `3` As 2 plus connection status + * * `4` Low-level data output + * @type integer * @see SMTP::$do_debug */ public $SMTPDebug = 0; /** - * The function/method to use for debugging output. - * Options: "echo" or "error_log" - * @type string + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @type string|callable * @see SMTP::$Debugoutput */ - public $Debugoutput = "echo"; + public $Debugoutput = 'echo'; /** * 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 bool + * @type boolean */ public $SMTPKeepAlive = false; /** * Whether to split multiple to addresses into multiple messages * or send them all in one message. - * @type bool + * @type boolean */ public $SingleTo = false; /** * Storage for addresses when SingleTo is enabled. * @type array - * @todo This should really not be public + * @TODO This should really not be public */ public $SingleToArray = array(); @@ -339,13 +365,14 @@ 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 - * @type bool + * @link http://www.postfix.org/VERP_README.html Postfix VERP info + * @type boolean */ public $do_verp = false; /** * Whether to allow sending messages with an empty body. - * @type bool + * @type boolean */ public $AllowEmpty = false; @@ -396,28 +423,23 @@ class PHPMailer * The function that handles the result of the send email action. * It is called out by send() for each email sent. * - * Value can be: - * - 'function_name' for function names - * - 'Class::Method' for static method calls - * - array($object, 'Method') for calling methods on $object - * See http://php.net/is_callable manual page for more details. + * Value can be any php callable: http://www.php.net/is_callable * * Parameters: - * bool $result result of the send action + * boolean $result result of the send action * string $to email address of the recipient * string $cc cc email addresses * string $bcc bcc email addresses * string $subject the subject * string $body the email body * string $from email address of sender - * * @type string */ public $action_function = ''; /** - * What to use in the X-Mailer header. - * Options: null for default, whitespace for none, or a string to use + * 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 */ public $XMailer = ''; @@ -459,7 +481,7 @@ class PHPMailer /** * An array of all kinds of addresses. - * Includes all of $to, $cc, $bcc, $replyto + * Includes all of $to, $cc, $bcc * @type array * @access protected */ @@ -528,6 +550,13 @@ class PHPMailer */ protected $sign_key_file = ''; + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * @type string + * @access protected + */ + protected $sign_extracerts_file = ''; + /** * The S/MIME password for the key. * Used only if the key is encrypted. @@ -538,38 +567,51 @@ class PHPMailer /** * Whether to throw exceptions for errors. - * @type bool + * @type boolean * @access protected */ protected $exceptions = false; /** - * Error severity: message only, continue processing + * Unique ID used for message ID and boundaries. + * @type string + * @access protected + */ + protected $uniqueid = ''; + + /** + * Error severity: message only, continue processing. */ const STOP_MESSAGE = 0; /** - * Error severity: message, likely ok to continue processing + * Error severity: message, likely ok to continue processing. */ const STOP_CONTINUE = 1; /** - * Error severity: message, plus full stop, critical error reached + * Error severity: message, plus full stop, critical error reached. */ const STOP_CRITICAL = 2; /** - * SMTP RFC standard line ending + * SMTP RFC standard line ending. */ const CRLF = "\r\n"; /** - * Constructor - * @param bool $exceptions Should we throw external exceptions? + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @type integer + */ + const MAX_LINE_LENGTH = 998; + + /** + * Constructor. + * @param boolean $exceptions Should we throw external exceptions? */ public function __construct($exceptions = false) { - $this->exceptions = ($exceptions == true); + $this->exceptions = (boolean)$exceptions; } /** @@ -577,7 +619,8 @@ class PHPMailer */ public function __destruct() { - if ($this->Mailer == 'smtp') { //close any open SMTP connection nicely + //Close any open SMTP connection nicely + if ($this->Mailer == 'smtp') { $this->smtpClose(); } } @@ -593,53 +636,75 @@ class PHPMailer * @param string $header Additional Header(s) * @param string $params Params * @access private - * @return bool + * @return boolean */ private function mailPassthru($to, $subject, $body, $header, $params) { + //Check overloading of mail function to avoid double-encoding + if (ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } if (ini_get('safe_mode') || !($this->UseSendmailOptions)) { - $rt = @mail($to, $this->encodeHeader($this->secureHeader($subject)), $body, $header); + $result = @mail($to, $subject, $body, $header); } else { - $rt = @mail($to, $this->encodeHeader($this->secureHeader($subject)), $body, $header, $params); + $result = @mail($to, $subject, $body, $header, $params); } - return $rt; + return $result; } /** * Output debugging info via user-defined method. - * Only if debug output is enabled. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). * @see PHPMailer::$Debugoutput * @see PHPMailer::$SMTPDebug * @param string $str */ protected function edebug($str) { - if (!$this->SMTPDebug) { + if ($this->SMTPDebug <= 0) { + return; + } + //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->SMTPDebug); return; } switch ($this->Debugoutput) { case 'error_log': + //Don't output, just log error_log($str); break; case 'html': - //Cleans up output a bit for a better looking display that's HTML-safe - echo htmlentities(preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, $this->CharSet) . "
\n"; + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ) + . "
\n"; break; case 'echo': default: - //Just echoes exactly what was received - echo $str; + //Normalize line breaks + $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s') . "\t" . str_replace( + "\n", + "\n \t ", + trim($str) + ) . "\n"; } } /** * Sets message type to HTML or plain. - * @param bool $ishtml True for HTML mode. + * @param boolean $isHtml True for HTML mode. * @return void */ - public function isHTML($ishtml = true) + public function isHTML($isHtml = true) { - if ($ishtml) { + if ($isHtml) { $this->ContentType = 'text/html'; } else { $this->ContentType = 'text/plain'; @@ -670,8 +735,12 @@ class PHPMailer */ public function isSendmail() { - if (!stristr(ini_get('sendmail_path'), 'sendmail')) { - $this->Sendmail = '/var/qmail/bin/sendmail'; + $ini_sendmail_path = ini_get('sendmail_path'); + + if (!stristr($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; } $this->Mailer = 'sendmail'; } @@ -682,17 +751,21 @@ class PHPMailer */ public function isQmail() { - if (stristr(ini_get('sendmail_path'), 'qmail')) { - $this->Sendmail = '/var/qmail/bin/sendmail'; + $ini_sendmail_path = ini_get('sendmail_path'); + + if (!stristr($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; } - $this->Mailer = 'sendmail'; + $this->Mailer = 'qmail'; } /** * Add a "To" address. * @param string $address * @param string $name - * @return bool true on success, false if address already used + * @return boolean true on success, false if address already used */ public function addAddress($address, $name = '') { @@ -704,7 +777,7 @@ class PHPMailer * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. * @param string $address * @param string $name - * @return bool true on success, false if address already used + * @return boolean true on success, false if address already used */ public function addCC($address, $name = '') { @@ -716,7 +789,7 @@ class PHPMailer * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. * @param string $address * @param string $name - * @return bool true on success, false if address already used + * @return boolean true on success, false if address already used */ public function addBCC($address, $name = '') { @@ -727,7 +800,7 @@ class PHPMailer * Add a "Reply-to" address. * @param string $address * @param string $name - * @return bool + * @return boolean */ public function addReplyTo($address, $name = '') { @@ -741,27 +814,27 @@ class PHPMailer * @param string $address The email address to send to * @param string $name * @throws phpmailerException - * @return bool true on success, false if address already used or invalid in some way + * @return boolean true on success, false if address already used or invalid in some way * @access protected */ 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 ($this->exceptions) { throw new phpmailerException('Invalid recipient array: ' . $kind); } - $this->edebug($this->lang('Invalid recipient array') . ': ' . $kind); 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); if ($this->exceptions) { throw new phpmailerException($this->lang('invalid_address') . ': ' . $address); } - $this->edebug($this->lang('invalid_address') . ': ' . $address); return false; } if ($kind != 'Reply-To') { @@ -783,9 +856,9 @@ class PHPMailer * Set the From and FromName properties. * @param string $address * @param string $name - * @param bool $auto Whether to also set the Sender address, defaults to true + * @param boolean $auto Whether to also set the Sender address, defaults to true * @throws phpmailerException - * @return bool + * @return boolean */ public function setFrom($address, $name = '', $auto = true) { @@ -793,10 +866,10 @@ class PHPMailer $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); if ($this->exceptions) { throw new phpmailerException($this->lang('invalid_address') . ': ' . $address); } - $this->edebug($this->lang('invalid_address') . ': ' . $address); return false; } $this->From = $address; @@ -825,27 +898,31 @@ 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 best one 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; faster, less thorough; - * 'noregex' - super fast, really dumb. - * @return bool + * * `auto` Pick strictest one 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; + * * `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. + * @return boolean * @static * @access public */ public static function validateAddress($address, $patternselect = 'auto') { - if ($patternselect == 'auto') { - if (defined( - 'PCRE_VERSION' - ) - ) { //Check this instead of extension_loaded so it works when that function is disabled - if (version_compare(PCRE_VERSION, '8.0') >= 0) { + 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 + if (defined('PCRE_VERSION')) { + //This pattern can get stuck in a recursive loop in PCRE <= 8.0.2 + if (version_compare(PCRE_VERSION, '8.0.3') >= 0) { $patternselect = 'pcre8'; } else { $patternselect = 'pcre'; } + } elseif (function_exists('extension_loaded') and extension_loaded('pcre')) { + //Fall back to older PCRE + $patternselect = 'pcre'; } else { //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension if (version_compare(PHP_VERSION, '5.2.0') >= 0) { @@ -858,14 +935,12 @@ class PHPMailer switch ($patternselect) { case 'pcre8': /** - * Conforms to RFC5322: Uses *correct* regex on which FILTER_VALIDATE_EMAIL is - * based; So why not use FILTER_VALIDATE_EMAIL? Because it was broken to - * not allow a@b type valid addresses :( + * Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains. * @link http://squiloople.com/2009/12/20/email-address-validation/ * @copyright 2009-2010 Michael Rushton * Feel free to use and redistribute this code. But please keep this copyright notice. */ - return (bool)preg_match( + return (boolean)preg_match( '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . @@ -877,10 +952,9 @@ class PHPMailer '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', $address ); - break; case 'pcre': //An older regex that doesn't need a recent PCRE - return (bool)preg_match( + return (boolean)preg_match( '/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' . '[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' . '(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' . @@ -893,27 +967,33 @@ class PHPMailer '|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD', $address ); - break; - case 'php': - default: - return (bool)filter_var($address, FILTER_VALIDATE_EMAIL); - break; + case 'html5': + /** + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (boolean)preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); case 'noregex': //No PCRE! Do something _very_ approximate! //Check the address is 3 chars or longer and contains an @ that's not the first or last char return (strlen($address) >= 3 and strpos($address, '@') >= 1 and strpos($address, '@') != strlen($address) - 1); - break; + case 'php': + default: + return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL); } } /** * Create a message and send it. * Uses the sending method specified by $Mailer. - * Returns false on error - Use the ErrorInfo variable to view description of the error. * @throws phpmailerException - * @return bool + * @return boolean false on error - See the ErrorInfo property for details of the error. */ public function send() { @@ -922,11 +1002,11 @@ class PHPMailer return false; } return $this->postSend(); - } catch (phpmailerException $e) { + } catch (phpmailerException $exc) { $this->mailHeader = ''; - $this->setError($e->getMessage()); + $this->setError($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } return false; } @@ -935,12 +1015,12 @@ class PHPMailer /** * Prepare a message for sending. * @throws phpmailerException - * @return bool + * @return boolean */ public function preSend() { try { - $this->mailHeader = ""; + $this->mailHeader = ''; if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) { throw new phpmailerException($this->lang('provide_address'), self::STOP_CRITICAL); } @@ -950,23 +1030,28 @@ class PHPMailer $this->ContentType = 'multipart/alternative'; } - $this->error_count = 0; // reset errors + $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)) { throw new phpmailerException($this->lang('empty_message'), self::STOP_CRITICAL); } - $this->MIMEHeader = $this->createHeader(); + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; // To capture the complete message when using mail(), create // an extra header list which createHeader() doesn't fold in if ($this->Mailer == 'mail') { if (count($this->to) > 0) { - $this->mailHeader .= $this->addrAppend("To", $this->to); + $this->mailHeader .= $this->addrAppend('To', $this->to); } else { - $this->mailHeader .= $this->headerLine("To", "undisclosed-recipients:;"); + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); } $this->mailHeader .= $this->headerLine( 'Subject', @@ -978,7 +1063,6 @@ class PHPMailer if (!empty($this->DKIM_domain) && !empty($this->DKIM_private) && !empty($this->DKIM_selector) - && !empty($this->DKIM_domain) && file_exists($this->DKIM_private)) { $header_dkim = $this->DKIM_Add( $this->MIMEHeader . $this->mailHeader, @@ -989,11 +1073,10 @@ class PHPMailer str_replace("\r\n", "\n", $header_dkim) . self::CRLF; } return true; - - } catch (phpmailerException $e) { - $this->setError($e->getMessage()); + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } return false; } @@ -1003,7 +1086,7 @@ class PHPMailer * Actually send a message. * Send the email via the selected mechanism * @throws phpmailerException - * @return bool + * @return boolean */ public function postSend() { @@ -1011,20 +1094,26 @@ class PHPMailer // Choose the mailer and send through it switch ($this->Mailer) { case 'sendmail': + case 'qmail': return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); case 'smtp': return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); case 'mail': return $this->mailSend($this->MIMEHeader, $this->MIMEBody); default: + $sendMethod = $this->Mailer.'Send'; + if (method_exists($this, $sendMethod)) { + return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); + } + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); } - } catch (phpmailerException $e) { - $this->setError($e->getMessage()); + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } - $this->edebug($e->getMessage() . "\n"); } return false; } @@ -1036,27 +1125,41 @@ class PHPMailer * @see PHPMailer::$Sendmail * @throws phpmailerException * @access protected - * @return bool + * @return boolean */ protected function sendmailSend($header, $body) { if ($this->Sender != '') { - $sendmail = sprintf("%s -oi -f%s -t", escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + if ($this->Mailer == 'qmail') { + $sendmail = sprintf('%s -f%s', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + } else { + $sendmail = sprintf('%s -oi -f%s -t', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender)); + } } else { - $sendmail = sprintf("%s -oi -t", escapeshellcmd($this->Sendmail)); + if ($this->Mailer == 'qmail') { + $sendmail = sprintf('%s', escapeshellcmd($this->Sendmail)); + } else { + $sendmail = sprintf('%s -oi -t', escapeshellcmd($this->Sendmail)); + } } - if ($this->SingleTo === true) { - foreach ($this->SingleToArray as $val) { + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { if (!@$mail = popen($sendmail, 'w')) { throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } - fputs($mail, "To: " . $val . "\n"); + fputs($mail, 'To: ' . $toAddr . "\n"); fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); - // implement call back function if it exists - $isSent = ($result == 0) ? 1 : 0; - $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $this->doCallback( + ($result == 0), + array($toAddr), + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); if ($result != 0) { throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } @@ -1068,9 +1171,7 @@ class PHPMailer fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); - // implement call back function if it exists - $isSent = ($result == 0) ? 1 : 0; - $this->doCallback($isSent, $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); } @@ -1085,43 +1186,39 @@ class PHPMailer * @link http://www.php.net/manual/en/book.mail.php * @throws phpmailerException * @access protected - * @return bool + * @return boolean */ protected function mailSend($header, $body) { $toArr = array(); - foreach ($this->to as $t) { - $toArr[] = $this->addrFormat($t); + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); } $to = implode(', ', $toArr); if (empty($this->Sender)) { - $params = " "; + $params = ' '; } else { - $params = sprintf("-f%s", $this->Sender); + $params = sprintf('-f%s', $this->Sender); } if ($this->Sender != '' and !ini_get('safe_mode')) { $old_from = ini_get('sendmail_from'); ini_set('sendmail_from', $this->Sender); } - $rt = false; - if ($this->SingleTo === true && count($toArr) > 1) { - foreach ($toArr as $val) { - $rt = $this->mailPassthru($val, $this->Subject, $body, $header, $params); - // implement call back function if it exists - $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent, $val, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $result = false; + if ($this->SingleTo && 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); } } else { - $rt = $this->mailPassthru($to, $this->Subject, $body, $header, $params); - // implement call back function if it exists - $isSent = ($rt == 1) ? 1 : 0; - $this->doCallback($isSent, $to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); } if (isset($old_from)) { ini_set('sendmail_from', $old_from); } - if (!$rt) { + if (!$result) { throw new phpmailerException($this->lang('instantiate'), self::STOP_CRITICAL); } return true; @@ -1135,7 +1232,7 @@ class PHPMailer public function getSMTPInstance() { if (!is_object($this->smtp)) { - require_once 'class-smtp.php'; + require_once( 'class-smtp.php' ); $this->smtp = new SMTP; } return $this->smtp; @@ -1151,62 +1248,59 @@ class PHPMailer * @throws phpmailerException * @uses SMTP * @access protected - * @return bool + * @return boolean */ protected function smtpSend($header, $body) { $bad_rcpt = array(); - - if (!$this->smtpConnect()) { + if (!$this->smtpConnect($this->SMTPOptions)) { throw new phpmailerException($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); } - $smtp_from = ($this->Sender == '') ? $this->From : $this->Sender; + if ('' == $this->Sender) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } if (!$this->smtp->mail($smtp_from)) { $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new phpmailerException($this->ErrorInfo, self::STOP_CRITICAL); } - // Attempt to send attach all recipients - foreach ($this->to as $to) { - if (!$this->smtp->recipient($to[0])) { - $bad_rcpt[] = $to[0]; - $isSent = 0; - } else { - $isSent = 1; - } - $this->doCallback($isSent, $to[0], '', '', $this->Subject, $body, $this->From); - } - foreach ($this->cc as $cc) { - if (!$this->smtp->recipient($cc[0])) { - $bad_rcpt[] = $cc[0]; - $isSent = 0; - } else { - $isSent = 1; - } - $this->doCallback($isSent, '', $cc[0], '', $this->Subject, $body, $this->From); - } - foreach ($this->bcc as $bcc) { - if (!$this->smtp->recipient($bcc[0])) { - $bad_rcpt[] = $bcc[0]; - $isSent = 0; - } else { - $isSent = 1; + // Attempt to send to all recipients + foreach (array($this->to, $this->cc, $this->bcc) as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0])) { + $error = $this->smtp->getError(); + $bad_rcpt[] = array('to' => $to[0], 'error' => $error['detail']); + $isSent = false; + } else { + $isSent = true; + } + $this->doCallback($isSent, array($to[0]), array(), array(), $this->Subject, $body, $this->From); } - $this->doCallback($isSent, '', '', $bcc[0], $this->Subject, $body, $this->From); } - if (count($bad_rcpt) > 0) { //Create error message for any bad addresses - throw new phpmailerException($this->lang('recipients_failed') . implode(', ', $bad_rcpt)); - } - if (!$this->smtp->data($header . $body)) { + // Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) { throw new phpmailerException($this->lang('data_not_accepted'), self::STOP_CRITICAL); } - if ($this->SMTPKeepAlive == true) { + if ($this->SMTPKeepAlive) { $this->smtp->reset(); } else { $this->smtp->quit(); $this->smtp->close(); } + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new phpmailerException( + $this->lang('recipients_failed') . $errstr, + self::STOP_CONTINUE + ); + } return true; } @@ -1217,7 +1311,7 @@ class PHPMailer * @uses SMTP * @access public * @throws phpmailerException - * @return bool + * @return boolean */ public function smtpConnect($options = array()) { @@ -1225,7 +1319,7 @@ class PHPMailer $this->smtp = $this->getSMTPInstance(); } - //Already connected? + // Already connected? if ($this->smtp->connected()) { return true; } @@ -1234,25 +1328,47 @@ class PHPMailer $this->smtp->setDebugLevel($this->SMTPDebug); $this->smtp->setDebugOutput($this->Debugoutput); $this->smtp->setVerp($this->do_verp); - $tls = ($this->SMTPSecure == 'tls'); - $ssl = ($this->SMTPSecure == 'ssl'); $hosts = explode(';', $this->Host); $lastexception = null; foreach ($hosts as $hostentry) { $hostinfo = array(); - $host = $hostentry; + if (!preg_match('/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*):?([0-9]*)$/', trim($hostentry), $hostinfo)) { + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = ($this->SMTPSecure == 'tls'); + if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; + } elseif ($hostinfo[2] == 'tls') { + $tls = true; + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA1'); + if ('tls' === $secure or 'ssl' === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new phpmailerException($this->lang('extension_missing').'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[3]; $port = $this->Port; - if (preg_match( - '/^(.+):([0-9]+)$/', - $hostentry, - $hostinfo - ) - ) { //If $hostentry contains 'address:port', override default - $host = $hostinfo[1]; - $port = $hostinfo[2]; + $tport = (integer)$hostinfo[4]; + if ($tport > 0 and $tport < 65536) { + $port = $tport; } - if ($this->smtp->connect(($ssl ? 'ssl://' : '') . $host, $port, $this->Timeout, $options)) { + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { try { if ($this->Helo) { $hello = $this->Helo; @@ -1260,12 +1376,19 @@ class PHPMailer $hello = $this->serverHostname(); } $this->smtp->hello($hello); - + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } if ($tls) { if (!$this->smtp->startTLS()) { throw new phpmailerException($this->lang('connect_host')); } - //We must resend HELO after tls negotiation + // We must resend HELO after tls negotiation $this->smtp->hello($hello); } if ($this->SMTPAuth) { @@ -1281,16 +1404,17 @@ class PHPMailer } } return true; - } catch (phpmailerException $e) { - $lastexception = $e; - //We must have connected, but then failed TLS or Auth, so close connection nicely + } catch (phpmailerException $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely $this->smtp->quit(); } } } - //If we get here, all connection attempts have failed, so close connection hard + // If we get here, all connection attempts have failed, so close connection hard $this->smtp->close(); - //As we've caught all exceptions, just report whatever the last one was + // As we've caught all exceptions, just report whatever the last one was if ($this->exceptions and !is_null($lastexception)) { throw $lastexception; } @@ -1317,12 +1441,12 @@ class PHPMailer * The default language is English. * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") * @param string $lang_path Path to the language file directory, with trailing separator (slash) - * @return bool + * @return boolean * @access public */ - public function setLanguage($langcode = 'en', $lang_path = 'language/') + public function setLanguage($langcode = 'en', $lang_path = '') { - //Define full set of translatable strings + // Define full set of translatable strings in English $PHPMAILER_LANG = array( 'authenticate' => 'SMTP Error: Could not authenticate.', 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', @@ -1341,16 +1465,28 @@ class PHPMailer 'signing' => 'Signing Error: ', 'smtp_connect_failed' => 'SMTP connect() failed.', 'smtp_error' => 'SMTP server error: ', - 'variable_set' => 'Cannot set or reset variable: ' + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ' ); - //Overwrite language-specific strings. - //This way we'll never have missing translations - no more "language string failed to load"! - $l = true; - if ($langcode != 'en') { //There is no English translation file - $l = @include $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + if (empty($lang_path)) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__FILE__). DIRECTORY_SEPARATOR . 'language'. DIRECTORY_SEPARATOR; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ($langcode != 'en') { + // Make sure language file path is readable + if (!is_readable($lang_file)) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } } $this->language = $PHPMAILER_LANG; - return ($l == true); //Returns false if language not found + return (boolean)$foundlang; // Returns false if language not found } /** @@ -1375,8 +1511,8 @@ class PHPMailer public function addrAppend($type, $addr) { $addresses = array(); - foreach ($addr as $a) { - $addresses[] = $this->addrFormat($a); + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); } return $type . ': ' . implode(', ', $addresses) . $this->LE; } @@ -1393,9 +1529,9 @@ class PHPMailer if (empty($addr[1])) { // No name provided return $this->secureHeader($addr[0]); } else { - return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . " <" . $this->secureHeader( + return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader( $addr[0] - ) . ">"; + ) . '>'; } } @@ -1406,47 +1542,54 @@ class PHPMailer * Original written by philippe. * @param string $message The message to wrap * @param integer $length The line length to wrap to - * @param bool $qp_mode Whether to run in Quoted-Printable mode + * @param boolean $qp_mode Whether to run in Quoted-Printable mode * @access public * @return string */ public function wrapText($message, $length, $qp_mode = false) { - $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE; + if ($qp_mode) { + $soft_break = sprintf(' =%s', $this->LE); + } else { + $soft_break = $this->LE; + } // If utf-8 encoding is used, we will need to make sure we don't // split multibyte characters when we wrap - $is_utf8 = (strtolower($this->CharSet) == "utf-8"); + $is_utf8 = (strtolower($this->CharSet) == 'utf-8'); $lelen = strlen($this->LE); $crlflen = strlen(self::CRLF); $message = $this->fixEOL($message); + //Remove a trailing line break if (substr($message, -$lelen) == $this->LE) { $message = substr($message, 0, -$lelen); } - $line = explode($this->LE, $message); // Magic. We know fixEOL uses $LE + //Split message into lines + $lines = explode($this->LE, $message); + //Message will be rebuilt in here $message = ''; - for ($i = 0; $i < count($line); $i++) { - $line_part = explode(' ', $line[$i]); + foreach ($lines as $line) { + $words = explode(' ', $line); $buf = ''; - for ($e = 0; $e < count($line_part); $e++) { - $word = $line_part[$e]; + $firstword = true; + foreach ($words as $word) { if ($qp_mode and (strlen($word) > $length)) { $space_left = $length - strlen($buf) - $crlflen; - if ($e != 0) { + if (!$firstword) { if ($space_left > 20) { $len = $space_left; if ($is_utf8) { $len = $this->utf8CharBoundary($word, $len); - } elseif (substr($word, $len - 1, 1) == "=") { + } elseif (substr($word, $len - 1, 1) == '=') { $len--; - } elseif (substr($word, $len - 2, 1) == "=") { + } elseif (substr($word, $len - 2, 1) == '=') { $len -= 2; } $part = substr($word, 0, $len); $word = substr($word, $len); $buf .= ' ' . $part; - $message .= $buf . sprintf("=%s", self::CRLF); + $message .= $buf . sprintf('=%s', self::CRLF); } else { $message .= $buf . $soft_break; } @@ -1459,29 +1602,33 @@ class PHPMailer $len = $length; if ($is_utf8) { $len = $this->utf8CharBoundary($word, $len); - } elseif (substr($word, $len - 1, 1) == "=") { + } elseif (substr($word, $len - 1, 1) == '=') { $len--; - } elseif (substr($word, $len - 2, 1) == "=") { + } elseif (substr($word, $len - 2, 1) == '=') { $len -= 2; } $part = substr($word, 0, $len); $word = substr($word, $len); if (strlen($word) > 0) { - $message .= $part . sprintf("=%s", self::CRLF); + $message .= $part . sprintf('=%s', self::CRLF); } else { $buf = $part; } } } else { $buf_o = $buf; - $buf .= ($e == 0) ? $word : (' ' . $word); + if (!$firstword) { + $buf .= ' '; + } + $buf .= $word; if (strlen($buf) > $length and $buf_o != '') { $message .= $buf_o . $soft_break; $buf = $word; } } + $firstword = false; } $message .= $buf . self::CRLF; } @@ -1491,12 +1638,12 @@ class PHPMailer /** * Find the last character boundary prior to $maxLength in a utf-8 - * quoted (printable) encoded string. + * quoted-printable encoded string. * Original written by Colin Brown. * @access public * @param string $encodedText utf-8 QP text - * @param int $maxLength find last character boundary prior to this length - * @return int + * @param integer $maxLength Find the last character boundary prior to this length + * @return integer */ public function utf8CharBoundary($encodedText, $maxLength) { @@ -1504,23 +1651,27 @@ class PHPMailer $lookBack = 3; while (!$foundSplitPos) { $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); - $encodedCharPos = strpos($lastChunk, "="); - if ($encodedCharPos !== false) { + $encodedCharPos = strpos($lastChunk, '='); + if (false !== $encodedCharPos) { // Found start of encoded character byte within $lookBack block. // Check the encoded byte value (the 2 chars after the '=') $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); $dec = hexdec($hex); - if ($dec < 128) { // Single byte character. + if ($dec < 128) { + // Single byte character. // If the encoded char was found at pos 0, it will fit // otherwise reduce maxLength to start of the encoded char - $maxLength = ($encodedCharPos == 0) ? $maxLength : - $maxLength - ($lookBack - $encodedCharPos); + if ($encodedCharPos > 0) { + $maxLength = $maxLength - ($lookBack - $encodedCharPos); + } $foundSplitPos = true; - } elseif ($dec >= 192) { // First byte of a multi byte character + } elseif ($dec >= 192) { + // First byte of a multi byte character // Reduce maxLength to split at start of character $maxLength = $maxLength - ($lookBack - $encodedCharPos); $foundSplitPos = true; - } elseif ($dec < 192) { // Middle byte of a multi byte character, look further back + } elseif ($dec < 192) { + // Middle byte of a multi byte character, look further back $lookBack += 3; } } else { @@ -1531,9 +1682,11 @@ class PHPMailer return $maxLength; } - /** - * Set the body wrapping. + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. * @access public * @return void */ @@ -1565,38 +1718,26 @@ class PHPMailer { $result = ''; - // Set the boundaries - $uniq_id = md5(uniqid(time())); - $this->boundary[1] = 'b1_' . $uniq_id; - $this->boundary[2] = 'b2_' . $uniq_id; - $this->boundary[3] = 'b3_' . $uniq_id; - if ($this->MessageDate == '') { - $result .= $this->headerLine('Date', self::rfcDate()); - } else { - $result .= $this->headerLine('Date', $this->MessageDate); + $this->MessageDate = self::rfcDate(); } + $result .= $this->headerLine('Date', $this->MessageDate); - if ($this->ReturnPath) { - $result .= $this->headerLine('Return-Path', '<' . trim($this->ReturnPath) . '>'); - } elseif ($this->Sender == '') { - $result .= $this->headerLine('Return-Path', '<' . trim($this->From) . '>'); - } else { - $result .= $this->headerLine('Return-Path', '<' . trim($this->Sender) . '>'); - } // To be created automatically by mail() - if ($this->Mailer != 'mail') { - if ($this->SingleTo === true) { - foreach ($this->to as $t) { - $this->SingleToArray[] = $this->addrFormat($t); + if ($this->SingleTo) { + if ($this->Mailer != 'mail') { + foreach ($this->to as $toaddr) { + $this->SingleToArray[] = $this->addrFormat($toaddr); } - } else { - if (count($this->to) > 0) { + } + } else { + if (count($this->to) > 0) { + if ($this->Mailer != 'mail') { $result .= $this->addrAppend('To', $this->to); - } elseif (count($this->cc) == 0) { - $result .= $this->headerLine('To', 'undisclosed-recipients:;'); } + } elseif (count($this->cc) == 0) { + $result .= $this->headerLine('To', 'undisclosed-recipients:;'); } } @@ -1608,7 +1749,11 @@ class PHPMailer } // sendmail and mail() extract Bcc from the header before sending - if ((($this->Mailer == 'sendmail') || ($this->Mailer == 'mail')) && (count($this->bcc) > 0)) { + if (( + $this->Mailer == 'sendmail' or $this->Mailer == 'qmail' or $this->Mailer == 'mail' + ) + and count($this->bcc) > 0 + ) { $result .= $this->addrAppend('Bcc', $this->bcc); } @@ -1624,9 +1769,9 @@ class PHPMailer if ($this->MessageID != '') { $this->lastMessageID = $this->MessageID; } else { - $this->lastMessageID = sprintf("<%s@%s>", $uniq_id, $this->ServerHostname()); + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->ServerHostname()); } - $result .= $this->HeaderLine('Message-ID', $this->lastMessageID); + $result .= $this->headerLine('Message-ID', $this->lastMessageID); $result .= $this->headerLine('X-Priority', $this->Priority); if ($this->XMailer == '') { $result .= $this->headerLine( @@ -1645,10 +1790,10 @@ class PHPMailer } // Add custom headers - for ($index = 0; $index < count($this->CustomHeader); $index++) { + foreach ($this->CustomHeader as $header) { $result .= $this->headerLine( - trim($this->CustomHeader[$index][0]), - $this->encodeHeader(trim($this->CustomHeader[$index][1])) + trim($header[0]), + $this->encodeHeader(trim($header[1])) ); } if (!$this->sign_key_file) { @@ -1667,6 +1812,7 @@ class PHPMailer public function getMailMIME() { $result = ''; + $ismultipart = true; switch ($this->message_type) { case 'inline': $result .= $this->headerLine('Content-Type', 'multipart/related;'); @@ -1687,11 +1833,20 @@ class PHPMailer default: // Catches case 'plain': and case '': $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); + $ismultipart = false; break; } - //RFC1341 part 5 says 7bit is assumed if not specified + // RFC1341 part 5 says 7bit is assumed if not specified if ($this->Encoding != '7bit') { - $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($ismultipart) { + if ($this->Encoding == '8bit') { + $result .= $this->headerLine('Content-Transfer-Encoding', '8bit'); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + } } if ($this->Mailer != 'mail') { @@ -1704,8 +1859,8 @@ class PHPMailer /** * Returns the whole MIME message. * Includes complete headers and body. - * Only valid post PreSend(). - * @see PHPMailer::PreSend() + * Only valid post preSend(). + * @see PHPMailer::preSend() * @access public * @return string */ @@ -1714,7 +1869,6 @@ class PHPMailer return $this->MIMEHeader . $this->mailHeader . self::CRLF . $this->MIMEBody; } - /** * Assemble the message body. * Returns an empty string on failure. @@ -1725,6 +1879,11 @@ class PHPMailer public function createBody() { $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = md5(uniqid(time())); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; if ($this->sign_key_file) { $body .= $this->getMailMIME() . $this->LE; @@ -1732,37 +1891,67 @@ class PHPMailer $this->setWordWrap(); + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ($bodyEncoding == '8bit' and !$this->has8bitChars($this->Body)) { + $bodyEncoding = '7bit'; + $bodyCharSet = 'us-ascii'; + } + //If lines are too long, change to quoted-printable transfer encoding + if (self::hasLineLongerThanMax($this->Body)) { + $this->Encoding = 'quoted-printable'; + $bodyEncoding = 'quoted-printable'; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ($altBodyEncoding == '8bit' and !$this->has8bitChars($this->AltBody)) { + $altBodyEncoding = '7bit'; + $altBodyCharSet = 'us-ascii'; + } + //If lines are too long, change to quoted-printable transfer encoding + if (self::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = 'quoted-printable'; + } + //Use this as a preamble in all multipart message types + $mimepre = "This is a multi-part message in MIME format." . $this->LE . $this->LE; switch ($this->message_type) { case 'inline': - $body .= $this->getBoundary($this->boundary[1], '', '', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[1]); break; case 'attach': - $body .= $this->getBoundary($this->boundary[1], '', '', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'inline_attach': + $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/related;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', '', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[2]); $body .= $this->LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'alt': - $body .= $this->getBoundary($this->boundary[1], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; - $body .= $this->getBoundary($this->boundary[1], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; if (!empty($this->Ical)) { $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', ''); @@ -1772,49 +1961,52 @@ class PHPMailer $body .= $this->endBoundary($this->boundary[1]); break; case 'alt_inline': - $body .= $this->getBoundary($this->boundary[1], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/related;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[2]); $body .= $this->LE; $body .= $this->endBoundary($this->boundary[1]); break; case 'alt_attach': + $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/alternative;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->endBoundary($this->boundary[2]); $body .= $this->LE; $body .= $this->attachAll('attachment', $this->boundary[1]); break; case 'alt_inline_attach': + $body .= $mimepre; $body .= $this->textLine('--' . $this->boundary[1]); $body .= $this->headerLine('Content-Type', 'multipart/alternative;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[2], '', 'text/plain', ''); - $body .= $this->encodeString($this->AltBody, $this->Encoding); + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->textLine('--' . $this->boundary[2]); $body .= $this->headerLine('Content-Type', 'multipart/related;'); $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"'); $body .= $this->LE; - $body .= $this->getBoundary($this->boundary[3], '', 'text/html', ''); - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); $body .= $this->LE . $this->LE; $body .= $this->attachAll('inline', $this->boundary[3]); $body .= $this->LE; @@ -1824,7 +2016,7 @@ class PHPMailer break; default: // catch case 'plain' and case '' - $body .= $this->encodeString($this->Body, $this->Encoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); break; } @@ -1833,31 +2025,51 @@ class PHPMailer } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { - throw new phpmailerException($this->lang('signing') . ' OpenSSL extension missing.'); + throw new phpmailerException($this->lang('extension_missing') . 'openssl'); } + // @TODO would be nice to use php://temp streams here, but need to wrap for PHP < 5.1 $file = tempnam(sys_get_temp_dir(), 'mail'); - file_put_contents($file, $body); //TODO check this worked + if (false === file_put_contents($file, $body)) { + throw new phpmailerException($this->lang('signing') . ' Could not write temp file'); + } $signed = tempnam(sys_get_temp_dir(), 'signed'); - if (@openssl_pkcs7_sign( - $file, - $signed, - 'file://' . realpath($this->sign_cert_file), - array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), - null - ) - ) { + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if (empty($this->sign_extracerts_file)) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), + null + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), + null, + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + if ($sign) { @unlink($file); $body = file_get_contents($signed); @unlink($signed); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . $this->LE . $this->LE; + $body = $parts[1]; } else { @unlink($file); @unlink($signed); throw new phpmailerException($this->lang('signing') . openssl_error_string()); } - } catch (phpmailerException $e) { + } catch (phpmailerException $exc) { $body = ''; if ($this->exceptions) { - throw $e; + throw $exc; } } } @@ -1886,9 +2098,12 @@ class PHPMailer $encoding = $this->Encoding; } $result .= $this->textLine('--' . $boundary); - $result .= sprintf("Content-Type: %s; charset=%s", $contentType, $charSet); + $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); $result .= $this->LE; - $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding != '7bit') { + $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + } $result .= $this->LE; return $result; @@ -1914,19 +2129,19 @@ class PHPMailer */ protected function setMessageType() { - $this->message_type = array(); + $type = array(); if ($this->alternativeExists()) { - $this->message_type[] = "alt"; + $type[] = 'alt'; } if ($this->inlineImageExists()) { - $this->message_type[] = "inline"; + $type[] = 'inline'; } if ($this->attachmentExists()) { - $this->message_type[] = "attach"; + $type[] = 'attach'; } - $this->message_type = implode("_", $this->message_type); - if ($this->message_type == "") { - $this->message_type = "plain"; + $this->message_type = implode('_', $type); + if ($this->message_type == '') { + $this->message_type = 'plain'; } } @@ -1962,7 +2177,7 @@ class PHPMailer * @param string $type File extension (MIME) type. * @param string $disposition Disposition to use * @throws phpmailerException - * @return bool + * @return boolean */ public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment') { @@ -1971,7 +2186,7 @@ class PHPMailer throw new phpmailerException($this->lang('file_access') . $path, self::STOP_CONTINUE); } - //If a MIME type is not specified, try to work it out from the file name + // If a MIME type is not specified, try to work it out from the file name if ($type == '') { $type = self::filenameToType($path); } @@ -1992,12 +2207,12 @@ class PHPMailer 7 => 0 ); - } catch (phpmailerException $e) { - $this->setError($e->getMessage()); + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); if ($this->exceptions) { - throw $e; + throw $exc; } - $this->edebug($e->getMessage() . "\n"); return false; } return true; @@ -2056,17 +2271,20 @@ class PHPMailer } $cidUniq[$cid] = true; - $mime[] = sprintf("--%s%s", $boundary, $this->LE); + $mime[] = sprintf('--%s%s', $boundary, $this->LE); $mime[] = sprintf( - "Content-Type: %s; name=\"%s\"%s", + 'Content-Type: %s; name="%s"%s', $type, $this->encodeHeader($this->secureHeader($name)), $this->LE ); - $mime[] = sprintf("Content-Transfer-Encoding: %s%s", $encoding, $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); + } if ($disposition == 'inline') { - $mime[] = sprintf("Content-ID: <%s>%s", $cid, $this->LE); + $mime[] = sprintf('Content-ID: <%s>%s', $cid, $this->LE); } // If a filename contains any of these chars, it should be quoted, @@ -2074,18 +2292,19 @@ class PHPMailer // Fixes a warning in IETF's msglint MIME checker // Allow for bypassing the Content-Disposition header totally if (!(empty($disposition))) { - if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $name)) { + $encoded_name = $this->encodeHeader($this->secureHeader($name)); + if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) { $mime[] = sprintf( - "Content-Disposition: %s; filename=\"%s\"%s", + 'Content-Disposition: %s; filename="%s"%s', $disposition, - $this->encodeHeader($this->secureHeader($name)), + $encoded_name, $this->LE . $this->LE ); } else { $mime[] = sprintf( - "Content-Disposition: %s; filename=%s%s", + 'Content-Disposition: %s; filename=%s%s', $disposition, - $this->encodeHeader($this->secureHeader($name)), + $encoded_name, $this->LE . $this->LE ); } @@ -2110,9 +2329,9 @@ class PHPMailer } } - $mime[] = sprintf("--%s--%s", $boundary, $this->LE); + $mime[] = sprintf('--%s--%s', $boundary, $this->LE); - return implode("", $mime); + return implode('', $mime); } /** @@ -2134,9 +2353,12 @@ class PHPMailer $magic_quotes = get_magic_quotes_runtime(); if ($magic_quotes) { if (version_compare(PHP_VERSION, '5.3.0', '<')) { - set_magic_quotes_runtime(0); + set_magic_quotes_runtime(false); } else { - ini_set('magic_quotes_runtime', 0); + //Doesn't exist in PHP 5.4, but we don't need to check because + //get_magic_quotes_runtime always returns false in 5.4+ + //so it will never get here + ini_set('magic_quotes_runtime', false); } } $file_buffer = file_get_contents($path); @@ -2149,8 +2371,8 @@ class PHPMailer } } return $file_buffer; - } catch (Exception $e) { - $this->setError($e->getMessage()); + } catch (Exception $exc) { + $this->setError($exc->getMessage()); return ''; } } @@ -2173,7 +2395,7 @@ class PHPMailer case '7bit': case '8bit': $encoded = $this->fixEOL($str); - //Make sure it ends with a line break + // Make sure it ends with a line break if (substr($encoded, -(strlen($this->LE))) != $this->LE) { $encoded .= $this->LE; } @@ -2201,11 +2423,11 @@ class PHPMailer */ public function encodeHeader($str, $position = 'text') { - $x = 0; + $matchcount = 0; switch (strtolower($position)) { case 'phrase': if (!preg_match('/[\200-\377]/', $str)) { - // Can't use addslashes as we don't know what value has magic_quotes_sybase + // Can't use addslashes as we don't know the value of magic_quotes_sybase $encoded = addcslashes($str, "\0..\37\177\\\""); if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { return ($encoded); @@ -2213,26 +2435,27 @@ class PHPMailer return ("\"$encoded\""); } } - $x = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); break; /** @noinspection PhpMissingBreakStatementInspection */ case 'comment': - $x = preg_match_all('/[()"]/', $str, $matches); + $matchcount = preg_match_all('/[()"]/', $str, $matches); // Intentional fall-through case 'text': default: - $x += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); break; } - if ($x == 0) { //There are no chars that need encoding + //There are no chars that need encoding + if ($matchcount == 0) { return ($str); } $maxlen = 75 - 7 - strlen($this->CharSet); // Try to select the encoding which should produce the shortest output - if ($x > strlen($str) / 3) { - //More than a third of the content will need encoding, so B encoding will be most efficient + if ($matchcount > strlen($str) / 3) { + // More than a third of the content will need encoding, so B encoding will be most efficient $encoding = 'B'; if (function_exists('mb_strlen') && $this->hasMultiBytes($str)) { // Use a custom function which correctly encodes and wraps long @@ -2250,7 +2473,7 @@ class PHPMailer $encoded = str_replace('=' . self::CRLF, "\n", trim($encoded)); } - $encoded = preg_replace('/^(.*)$/m', " =?" . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); $encoded = trim(str_replace("\n", $this->LE, $encoded)); return $encoded; @@ -2260,7 +2483,7 @@ class PHPMailer * Check if a string contains multi-byte characters. * @access public * @param string $str multi-byte text to wrap encode - * @return bool + * @return boolean */ public function hasMultiBytes($str) { @@ -2271,22 +2494,33 @@ class PHPMailer } } + /** + * Does a string contain any 8-bit chars (in any charset)? + * @param string $text + * @return boolean + */ + public function has8bitChars($text) + { + return (boolean)preg_match('/[\x80-\xFF]/', $text); + } + /** * Encode and wrap long multibyte strings for mail headers * without breaking lines within a character. - * Adapted from a function by paravoid at http://uk.php.net/manual/en/function.mb-encode-mimeheader.php + * Adapted from a function by paravoid + * @link http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 * @access public * @param string $str multi-byte text to wrap encode - * @param string $lf string to use as linefeed/end-of-line + * @param string $linebreak string to use as linefeed/end-of-line * @return string */ - public function base64EncodeWrapMB($str, $lf = null) + public function base64EncodeWrapMB($str, $linebreak = null) { - $start = "=?" . $this->CharSet . "?B?"; - $end = "?="; - $encoded = ""; - if ($lf === null) { - $lf = $this->LE; + $start = '=?' . $this->CharSet . '?B?'; + $end = '?='; + $encoded = ''; + if ($linebreak === null) { + $linebreak = $this->LE; } $mb_length = mb_strlen($str, $this->CharSet); @@ -2305,11 +2539,11 @@ class PHPMailer $chunk = base64_encode($chunk); $lookBack++; } while (strlen($chunk) > $length); - $encoded .= $chunk . $lf; + $encoded .= $chunk . $linebreak; } // Chomp the last linefeed - $encoded = substr($encoded, 0, -strlen($lf)); + $encoded = substr($encoded, 0, -strlen($linebreak)); return $encoded; } @@ -2320,21 +2554,22 @@ class PHPMailer * @param string $string The text to encode * @param integer $line_max Number of chars allowed on a line before wrapping * @return string - * @link PHP version adapted from http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 + * @link http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 Adapted from this comment */ public function encodeQP($string, $line_max = 76) { - if (function_exists('quoted_printable_encode')) { //Use native function if it's available (>= PHP5.3) - return quoted_printable_encode($string); + // Use native function if it's available (>= PHP5.3) + if (function_exists('quoted_printable_encode')) { + return $this->fixEOL(quoted_printable_encode($string)); } - //Fall back to a pure PHP implementation + // Fall back to a pure PHP implementation $string = str_replace( array('%20', '%0D%0A.', '%0D%0A', '%'), array(' ', "\r\n=2E", "\r\n", '='), rawurlencode($string) ); $string = preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string); - return $string; + return $this->fixEOL($string); } /** @@ -2343,7 +2578,7 @@ class PHPMailer * @access public * @param string $string * @param integer $line_max - * @param bool $space_conv + * @param boolean $space_conv * @return string * @deprecated Use encodeQP instead. */ @@ -2365,41 +2600,41 @@ class PHPMailer */ public function encodeQ($str, $position = 'text') { - //There should not be any EOL in the string + // There should not be any EOL in the string $pattern = ''; $encoded = str_replace(array("\r", "\n"), '', $str); switch (strtolower($position)) { case 'phrase': - //RFC 2047 section 5.3 + // RFC 2047 section 5.3 $pattern = '^A-Za-z0-9!*+\/ -'; break; /** @noinspection PhpMissingBreakStatementInspection */ case 'comment': - //RFC 2047 section 5.2 + // RFC 2047 section 5.2 $pattern = '\(\)"'; - //intentional fall-through - //for this reason we build the $pattern without including delimiters and [] + // intentional fall-through + // for this reason we build the $pattern without including delimiters and [] case 'text': default: - //RFC 2047 section 5.1 - //Replace every high ascii, control, =, ? and _ characters + // RFC 2047 section 5.1 + // Replace every high ascii, control, =, ? and _ characters $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; break; } $matches = array(); if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { - //If the string contains an '=', make sure it's the first thing we replace - //so as to avoid double-encoding - $s = array_search('=', $matches[0]); - if ($s !== false) { - unset($matches[0][$s]); + // If the string contains an '=', make sure it's the first thing we replace + // so as to avoid double-encoding + $eqkey = array_search('=', $matches[0]); + if (false !== $eqkey) { + unset($matches[0][$eqkey]); array_unshift($matches[0], '='); } foreach (array_unique($matches[0]) as $char) { $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); } } - //Replace every spaces to _ (more readable than =20) + // Replace every spaces to _ (more readable than =20) return str_replace(' ', '_', $encoded); } @@ -2422,7 +2657,7 @@ class PHPMailer $type = '', $disposition = 'attachment' ) { - //If a MIME type is not specified, try to work it out from the file name + // If a MIME type is not specified, try to work it out from the file name if ($type == '') { $type = self::filenameToType($filename); } @@ -2442,7 +2677,7 @@ class PHPMailer /** * Add an embedded (inline) attachment from a file. * This can include images, sounds, and just about any other document type. - * These differ from 'regular' attachmants in that they are intended to be + * These differ from 'regular' attachments in that they are intended to be * displayed inline with the message, not just attached for download. * This is used in HTML messages that embed the images * the HTML refers to using the $cid value. @@ -2453,7 +2688,7 @@ class PHPMailer * @param string $encoding File encoding (see $Encoding). * @param string $type File MIME type. * @param string $disposition Disposition to use - * @return bool True on successfully adding an attachment + * @return boolean True on successfully adding an attachment */ public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline') { @@ -2462,7 +2697,7 @@ class PHPMailer return false; } - //If a MIME type is not specified, try to work it out from the file name + // If a MIME type is not specified, try to work it out from the file name if ($type == '') { $type = self::filenameToType($path); } @@ -2498,7 +2733,7 @@ class PHPMailer * @param string $encoding File encoding (see $Encoding). * @param string $type MIME type. * @param string $disposition Disposition to use - * @return bool True on successfully adding an attachment + * @return boolean True on successfully adding an attachment */ public function addStringEmbeddedImage( $string, @@ -2508,7 +2743,7 @@ class PHPMailer $type = '', $disposition = 'inline' ) { - //If a MIME type is not specified, try to work it out from the name + // If a MIME type is not specified, try to work it out from the name if ($type == '') { $type = self::filenameToType($name); } @@ -2530,7 +2765,7 @@ class PHPMailer /** * Check if an inline attachment is present. * @access public - * @return bool + * @return boolean */ public function inlineImageExists() { @@ -2544,7 +2779,7 @@ class PHPMailer /** * Check if an attachment (non-inline) is present. - * @return bool + * @return boolean */ public function attachmentExists() { @@ -2558,7 +2793,7 @@ class PHPMailer /** * Check if this message has an alternative body set. - * @return bool + * @return boolean */ public function alternativeExists() { @@ -2651,8 +2886,17 @@ class PHPMailer $this->error_count++; if ($this->Mailer == 'smtp' and !is_null($this->smtp)) { $lasterror = $this->smtp->getError(); - if (!empty($lasterror) and array_key_exists('smtp_msg', $lasterror)) { - $msg .= '

' . $this->lang('smtp_error') . $lasterror['smtp_msg'] . "

\n"; + if (!empty($lasterror['error'])) { + $msg .= $this->lang('smtp_error') . $lasterror['error']; + if (!empty($lasterror['detail'])) { + $msg .= ' Detail: '. $lasterror['detail']; + } + if (!empty($lasterror['smtp_code'])) { + $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + } + if (!empty($lasterror['smtp_code_ex'])) { + $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + } } } $this->ErrorInfo = $msg; @@ -2666,8 +2910,8 @@ class PHPMailer */ public static function rfcDate() { - //Set the time zone to whatever the default is to avoid 500 errors - //Will default to UTC if it's not set properly in php.ini + // Set the time zone to whatever the default is to avoid 500 errors + // Will default to UTC if it's not set properly in php.ini date_default_timezone_set(@date_default_timezone_get()); return date('D, j M Y H:i:s O'); } @@ -2680,14 +2924,16 @@ class PHPMailer */ protected function serverHostname() { + $result = 'localhost.localdomain'; if (!empty($this->Hostname)) { $result = $this->Hostname; - } elseif (isset($_SERVER['SERVER_NAME'])) { + } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER) and !empty($_SERVER['SERVER_NAME'])) { $result = $_SERVER['SERVER_NAME']; - } else { - $result = 'localhost.localdomain'; + } elseif (function_exists('gethostname') && gethostname() !== false) { + $result = gethostname(); + } elseif (php_uname('n') !== false) { + $result = php_uname('n'); } - return $result; } @@ -2703,17 +2949,24 @@ class PHPMailer $this->setLanguage('en'); // set the default language } - if (isset($this->language[$key])) { + if (array_key_exists($key, $this->language)) { + if ($key == 'smtp_connect_failed') { + //Include a link to troubleshooting docs on SMTP connection failure + //this is by far the biggest cause of support questions + //but it's usually not PHPMailer's fault. + return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + } return $this->language[$key]; } else { - return 'Language string failed to load: ' . $key; + //Return the key as a fallback + return $key; } } /** * Check if an error occurred. * @access public - * @return bool True if an error did occur. + * @return boolean True if an error did occur. */ public function isError() { @@ -2757,6 +3010,16 @@ class PHPMailer } } + /** + * Returns all custom headers + * + * @return array + */ + public function getCustomHeaders() + { + return $this->CustomHeader; + } + /** * Create a message from an HTML string. * Automatically makes modifications for inline images and backgrounds @@ -2765,22 +3028,39 @@ class PHPMailer * @access public * @param string $message HTML message string * @param string $basedir baseline directory for path - * @param bool $advanced Whether to use the advanced HTML to text converter + * @param boolean|callable $advanced Whether to use the internal HTML to text converter + * or your own custom converter @see html2text() * @return string $message */ public function msgHTML($message, $basedir = '', $advanced = false) { - preg_match_all("/(src|background)=[\"'](.*)[\"']/Ui", $message, $images); + preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); if (isset($images[2])) { - foreach ($images[2] as $i => $url) { - // do not change urls for absolute images (thanks to corvuscorax) - if (!preg_match('#^[A-z]+://#', $url)) { + foreach ($images[2] as $imgindex => $url) { + // Convert data URIs into embedded images + if (preg_match('#^data:(image[^;,]*)(;base64)?,#', $url, $match)) { + $data = substr($url, strpos($url, ',')); + if ($match[2]) { + $data = base64_decode($data); + } else { + $data = rawurldecode($data); + } + $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 + if ($this->addStringEmbeddedImage($data, $cid, '', 'base64', $match[1])) { + $message = str_replace( + $images[0][$imgindex], + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + } + } elseif (!preg_match('#^[A-z]+://#', $url)) { + // Do not change urls for absolute images (thanks to corvuscorax) $filename = basename($url); $directory = dirname($url); if ($directory == '.') { $directory = ''; } - $cid = md5($url) . '@phpmailer.0'; //RFC2392 S 2 + $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 if (strlen($basedir) > 1 && substr($basedir, -1) != '/') { $basedir .= '/'; } @@ -2792,12 +3072,12 @@ class PHPMailer $cid, $filename, 'base64', - self::_mime_types(self::mb_pathinfo($filename, PATHINFO_EXTENSION)) + self::_mime_types((string)self::mb_pathinfo($filename, PATHINFO_EXTENSION)) ) ) { $message = preg_replace( - "/" . $images[1][$i] . "=[\"']" . preg_quote($url, '/') . "[\"']/Ui", - $images[1][$i] . "=\"cid:" . $cid . "\"", + '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', + $images[1][$imgindex] . '="cid:' . $cid . '"', $message ); } @@ -2805,27 +3085,40 @@ class PHPMailer } } $this->isHTML(true); - if (empty($this->AltBody)) { - $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . "\n\n"; - } - //Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better + // 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)) { + $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . + self::CRLF . self::CRLF; + } return $this->Body; } /** * 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 + * Example usage: + * + * // Use default conversion + * $plain = $mail->html2text($html); + * // Use your own custom converter + * $plain = $mail->html2text($html, function($html) { + * $converter = new MyHtml2text($html); + * return $converter->get_text(); + * }); + * * @param string $html The HTML text to convert - * @param bool $advanced Should this use the more complex html2text converter or just a simple one? + * @param boolean|callable $advanced Any boolean value to use the internal converter, + * or provide your own callable for custom conversion. * @return string */ public function html2text($html, $advanced = false) { - if ($advanced) { - require_once 'extras/class.html2text.php'; - $h = new html2text($html); - return $h->get_text(); + if (is_callable($advanced)) { + return call_user_func($advanced, $html); } return html_entity_decode( trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), @@ -2844,94 +3137,99 @@ class PHPMailer public static function _mime_types($ext = '') { $mimes = array( - 'xl' => 'application/excel', - 'hqx' => 'application/mac-binhex40', - 'cpt' => 'application/mac-compactpro', - 'bin' => 'application/macbinary', - 'doc' => 'application/msword', - 'word' => 'application/msword', + 'xl' => 'application/excel', + 'js' => 'application/javascript', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'bin' => 'application/macbinary', + 'doc' => 'application/msword', + 'word' => 'application/msword', 'class' => 'application/octet-stream', - 'dll' => 'application/octet-stream', - 'dms' => 'application/octet-stream', - 'exe' => 'application/octet-stream', - 'lha' => 'application/octet-stream', - 'lzh' => 'application/octet-stream', - 'psd' => 'application/octet-stream', - 'sea' => 'application/octet-stream', - 'so' => 'application/octet-stream', - 'oda' => 'application/oda', - 'pdf' => 'application/pdf', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'mif' => 'application/vnd.mif', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'psd' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'so' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', 'wbxml' => 'application/vnd.wap.wbxml', - 'wmlc' => 'application/vnd.wap.wmlc', - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dxr' => 'application/x-director', - 'dvi' => 'application/x-dvi', - 'gtar' => 'application/x-gtar', - 'php3' => 'application/x-httpd-php', - 'php4' => 'application/x-httpd-php', - 'php' => 'application/x-httpd-php', + 'wmlc' => 'application/vnd.wap.wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php' => 'application/x-httpd-php', 'phtml' => 'application/x-httpd-php', - 'phps' => 'application/x-httpd-php-source', - 'js' => 'application/x-javascript', - 'swf' => 'application/x-shockwave-flash', - 'sit' => 'application/x-stuffit', - 'tar' => 'application/x-tar', - 'tgz' => 'application/x-tar', - 'xht' => 'application/xhtml+xml', + 'phps' => 'application/x-httpd-php-source', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'xht' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml', - 'zip' => 'application/zip', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mpga' => 'audio/mpeg', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'ram' => 'audio/x-pn-realaudio', - 'rm' => 'audio/x-pn-realaudio', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'ra' => 'audio/x-realaudio', - 'wav' => 'audio/x-wav', - 'bmp' => 'image/bmp', - 'gif' => 'image/gif', - 'jpeg' => 'image/jpeg', - 'jpe' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'png' => 'image/png', - 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'eml' => 'message/rfc822', - 'css' => 'text/css', - 'html' => 'text/html', - 'htm' => 'text/html', + 'zip' => 'application/zip', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpga' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'wav' => 'audio/x-wav', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'eml' => 'message/rfc822', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', 'shtml' => 'text/html', - 'log' => 'text/plain', - 'text' => 'text/plain', - 'txt' => 'text/plain', - 'rtx' => 'text/richtext', - 'rtf' => 'text/rtf', - 'xml' => 'text/xml', - 'xsl' => 'text/xml', - 'mpeg' => 'video/mpeg', - 'mpe' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mov' => 'video/quicktime', - 'qt' => 'video/quicktime', - 'rv' => 'video/vnd.rn-realvideo', - 'avi' => 'video/x-msvideo', + 'log' => 'text/plain', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'mpeg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'rv' => 'video/vnd.rn-realvideo', + 'avi' => 'video/x-msvideo', 'movie' => 'video/x-sgi-movie' ); - return (array_key_exists(strtolower($ext), $mimes) ? $mimes[strtolower($ext)]: 'application/octet-stream'); + if (array_key_exists(strtolower($ext), $mimes)) { + return $mimes[strtolower($ext)]; + } + return 'application/octet-stream'; } /** @@ -2943,9 +3241,9 @@ class PHPMailer */ public static function filenameToType($filename) { - //In case the path is a URL, strip any query string before getting extension + // In case the path is a URL, strip any query string before getting extension $qpos = strpos($filename, '?'); - if ($qpos !== false) { + if (false !== $qpos) { $filename = substr($filename, 0, $qpos); } $pathinfo = self::mb_pathinfo($filename); @@ -2966,37 +3264,34 @@ class PHPMailer public static function mb_pathinfo($path, $options = null) { $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); - $m = array(); - preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $m); - if (array_key_exists(1, $m)) { - $ret['dirname'] = $m[1]; - } - if (array_key_exists(2, $m)) { - $ret['basename'] = $m[2]; - } - if (array_key_exists(5, $m)) { - $ret['extension'] = $m[5]; - } - if (array_key_exists(3, $m)) { - $ret['filename'] = $m[3]; + $pathinfo = array(); + if (preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $pathinfo)) { + if (array_key_exists(1, $pathinfo)) { + $ret['dirname'] = $pathinfo[1]; + } + if (array_key_exists(2, $pathinfo)) { + $ret['basename'] = $pathinfo[2]; + } + if (array_key_exists(5, $pathinfo)) { + $ret['extension'] = $pathinfo[5]; + } + if (array_key_exists(3, $pathinfo)) { + $ret['filename'] = $pathinfo[3]; + } } switch ($options) { case PATHINFO_DIRNAME: case 'dirname': return $ret['dirname']; - break; case PATHINFO_BASENAME: case 'basename': return $ret['basename']; - break; case PATHINFO_EXTENSION: case 'extension': return $ret['extension']; - break; case PATHINFO_FILENAME: case 'filename': return $ret['filename']; - break; default: return $ret; } @@ -3004,33 +3299,27 @@ class PHPMailer /** * Set or reset instance properties. - * + * You should avoid this function - it's more verbose, less efficient, more error-prone and + * harder to debug than setting properties directly. * Usage Example: - * $page->set('X-Priority', '3'); - * + * `$mail->set('SMTPSecure', 'tls');` + * is the same as: + * `$mail->SMTPSecure = 'tls';` * @access public - * @param string $name - * @param mixed $value - * NOTE: will not work with arrays, there are no arrays to set/reset - * @throws phpmailerException - * @return bool - * @todo Should this not be using __set() magic function? + * @param string $name The property name to set + * @param mixed $value The value to set the property to + * @return boolean + * @TODO Should this not be using the __set() magic function? */ public function set($name, $value = '') { - try { - if (isset($this->$name)) { - $this->$name = $value; - } else { - throw new phpmailerException($this->lang('variable_set') . $name, self::STOP_CRITICAL); - } - } catch (Exception $e) { - $this->setError($e->getMessage()); - if ($e->getCode() == self::STOP_CRITICAL) { - return false; - } + if (property_exists($this, $name)) { + $this->$name = $value; + return true; + } else { + $this->setError($this->lang('variable_set') . $name); + return false; } - return true; } /** @@ -3061,17 +3350,19 @@ class PHPMailer /** - * Set the private key file and password for S/MIME signing. + * Set the public and private key files and password for S/MIME signing. * @access public * @param string $cert_filename * @param string $key_filename * @param string $key_pass Password for private key + * @param string $extracerts_filename Optional path to chain certificate */ - public function sign($cert_filename, $key_filename, $key_pass) + public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') { $this->sign_cert_file = $cert_filename; $this->sign_key_file = $key_filename; $this->sign_key_pass = $key_pass; + $this->sign_extracerts_file = $extracerts_filename; } /** @@ -3088,7 +3379,7 @@ class PHPMailer if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord == 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) { $line .= $txt[$i]; } else { - $line .= "=" . sprintf("%02X", $ord); + $line .= '=' . sprintf('%02X', $ord); } } return $line; @@ -3097,15 +3388,15 @@ class PHPMailer /** * Generate a DKIM signature. * @access public - * @param string $s Header + * @param string $signHeader * @throws phpmailerException * @return string */ - public function DKIM_Sign($s) + public function DKIM_Sign($signHeader) { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { - throw new phpmailerException($this->lang("signing") . ' OpenSSL extension missing.'); + throw new phpmailerException($this->lang('extension_missing') . 'openssl'); } return ''; } @@ -3115,7 +3406,7 @@ class PHPMailer } else { $privKey = $privKeyStr; } - if (openssl_sign($s, $signature, $privKey)) { + if (openssl_sign($signHeader, $signature, $privKey)) { return base64_encode($signature); } return ''; @@ -3124,21 +3415,21 @@ class PHPMailer /** * Generate a DKIM canonicalization header. * @access public - * @param string $s Header + * @param string $signHeader Header * @return string */ - public function DKIM_HeaderC($s) + public function DKIM_HeaderC($signHeader) { - $s = preg_replace("/\r\n\s+/", " ", $s); - $lines = explode("\r\n", $s); + $signHeader = preg_replace('/\r\n\s+/', ' ', $signHeader); + $lines = explode("\r\n", $signHeader); foreach ($lines as $key => $line) { - list($heading, $value) = explode(":", $line, 2); + list($heading, $value) = explode(':', $line, 2); $heading = strtolower($heading); - $value = preg_replace("/\s+/", " ", $value); // Compress useless spaces - $lines[$key] = $heading . ":" . trim($value); // Don't forget to remove WSP around the value + $value = preg_replace('/\s+/', ' ', $value); // Compress useless spaces + $lines[$key] = $heading . ':' . trim($value); // Don't forget to remove WSP around the value } - $s = implode("\r\n", $lines); - return $s; + $signHeader = implode("\r\n", $lines); + return $signHeader; } /** @@ -3189,8 +3480,8 @@ class PHPMailer $to_header = $header; $current = 'to_header'; } else { - if ($current && strpos($header, ' =?') === 0) { - $current .= $header; + if (!empty($$current) && strpos($header, ' =?') === 0) { + $$current .= $header; } else { $current = ''; } @@ -3205,17 +3496,21 @@ 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 - $ident = ($this->DKIM_identity == '') ? '' : " i=" . $this->DKIM_identity . ";"; - $dkimhdrs = "DKIM-Signature: v=1; a=" . - $DKIMsignatureType . "; q=" . - $DKIMquery . "; l=" . - $DKIMlen . "; s=" . + $DKIMb64 = base64_encode(pack('H*', sha1($body))); // Base64 of packed binary SHA-1 hash of body + if ('' == $this->DKIM_identity) { + $ident = ''; + } else { + $ident = ' i=' . $this->DKIM_identity . ';'; + } + $dkimhdrs = 'DKIM-Signature: v=1; a=' . + $DKIMsignatureType . '; q=' . + $DKIMquery . '; l=' . + $DKIMlen . '; s=' . $this->DKIM_selector . ";\r\n" . - "\tt=" . $DKIMtime . "; c=" . $DKIMcanonicalization . ";\r\n" . + "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . "\th=From:To:Subject;\r\n" . - "\td=" . $this->DKIM_domain . ";" . $ident . "\r\n" . + "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . "\tz=$from\r\n" . "\t|$to\r\n" . "\t|$subject;\r\n" . @@ -3228,17 +3523,79 @@ class PHPMailer return $dkimhdrs . $signed . "\r\n"; } + /** + * Detect if a string contains a line longer than the maximum line length allowed. + * @param string $str + * @return boolean + * @static + */ + public static function hasLineLongerThanMax($str) + { + //+2 to include CRLF line break for a 1000 total + return (boolean)preg_match('/^(.{'.(self::MAX_LINE_LENGTH + 2).',})/m', $str); + } + + /** + * Allows for public read access to 'to' property. + * @access public + * @return array + */ + public function getToAddresses() + { + return $this->to; + } + + /** + * Allows for public read access to 'cc' property. + * @access public + * @return array + */ + public function getCcAddresses() + { + return $this->cc; + } + + /** + * Allows for public read access to 'bcc' property. + * @access public + * @return array + */ + public function getBccAddresses() + { + return $this->bcc; + } + + /** + * Allows for public read access to 'ReplyTo' property. + * @access public + * @return array + */ + public function getReplyToAddresses() + { + return $this->ReplyTo; + } + + /** + * Allows for public read access to 'all_recipients' property. + * @access public + * @return array + */ + public function getAllRecipientAddresses() + { + return $this->all_recipients; + } + /** * Perform a callback. - * @param bool $isSent - * @param string $to - * @param string $cc - * @param string $bcc + * @param boolean $isSent + * @param array $to + * @param array $cc + * @param array $bcc * @param string $subject * @param string $body * @param string $from */ - protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from = null) + protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from) { if (!empty($this->action_function) && is_callable($this->action_function)) { $params = array($isSent, $to, $cc, $bcc, $subject, $body, $from);