diff options
| -rw-r--r-- | lib/PHPMailer/PHPMailer/Exception.php | 2 | ||||
| -rw-r--r-- | lib/PHPMailer/PHPMailer/PHPMailer.php | 210 | ||||
| -rw-r--r-- | lib/PHPMailer/PHPMailer/SMTP.php | 5 |
3 files changed, 158 insertions, 59 deletions
diff --git a/lib/PHPMailer/PHPMailer/Exception.php b/lib/PHPMailer/PHPMailer/Exception.php index a50a8991f..52eaf9515 100644 --- a/lib/PHPMailer/PHPMailer/Exception.php +++ b/lib/PHPMailer/PHPMailer/Exception.php @@ -35,6 +35,6 @@ class Exception extends \Exception */ public function errorMessage() { - return '<strong>' . htmlspecialchars($this->getMessage()) . "</strong><br />\n"; + return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n"; } } diff --git a/lib/PHPMailer/PHPMailer/PHPMailer.php b/lib/PHPMailer/PHPMailer/PHPMailer.php index 62553fd46..5b6dcfad6 100644 --- a/lib/PHPMailer/PHPMailer/PHPMailer.php +++ b/lib/PHPMailer/PHPMailer/PHPMailer.php @@ -103,14 +103,14 @@ class PHPMailer * * @var string */ - public $From = 'root@localhost'; + public $From = ''; /** * The From name of the message. * * @var string */ - public $FromName = 'Root User'; + public $FromName = ''; /** * The envelope sender of the message. @@ -428,9 +428,11 @@ class PHPMailer 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(). + * Whether to keep the SMTP connection open after each message. + * If this is set to true then the connection will remain open after a send, + * and closing the connection will require an explicit call to smtpClose(). + * It's a good idea to use this if you are sending multiple messages as it reduces overhead. + * See the mailing list example for how to use it. * * @var bool */ @@ -687,7 +689,7 @@ class PHPMailer protected $boundary = []; /** - * The array of available languages. + * The array of available text strings for the current language. * * @var array */ @@ -748,7 +750,7 @@ class PHPMailer * * @var string */ - const VERSION = '6.3.0'; + const VERSION = '6.5.1'; /** * Error severity: message only, continue processing. @@ -1186,21 +1188,33 @@ class PHPMailer * * @return array */ - public static function parseAddresses($addrstr, $useimap = true) + public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) { $addresses = []; if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { //Use this built-in parser if it's available $list = imap_rfc822_parse_adrlist($addrstr, ''); + // Clear any potential IMAP errors to get rid of notices being thrown at end of script. + imap_errors(); foreach ($list as $address) { if ( - ('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress( - $address->mailbox . '@' . $address->host - ) + '.SYNTAX-ERROR.' !== $address->host && + static::validateAddress($address->mailbox . '@' . $address->host) ) { //Decode the name part if it's present and encoded - if (property_exists($address, 'personal') && preg_match('/^=\?.*\?=$/', $address->personal)) { + if ( + property_exists($address, 'personal') && + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + defined('MB_CASE_UPPER') && + preg_match('/^=\?.*\?=$/s', $address->personal) + ) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $address->personal = str_replace('_', '=20', $address->personal); + //Decode the name $address->personal = mb_decode_mimeheader($address->personal); + mb_internal_encoding($origCharset); } $addresses[] = [ @@ -1228,9 +1242,16 @@ class PHPMailer $email = trim(str_replace('>', '', $email)); $name = trim($name); if (static::validateAddress($email)) { + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled //If this name is encoded, decode it - if (preg_match('/^=\?.*\?=$/', $name)) { + if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $name = str_replace('_', '=20', $name); + //Decode the name $name = mb_decode_mimeheader($name); + mb_internal_encoding($origCharset); } $addresses[] = [ //Remove any surrounding quotes and spaces from the name @@ -1331,7 +1352,8 @@ class PHPMailer if (null === $patternselect) { $patternselect = static::$validator; } - if (is_callable($patternselect)) { + //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603 + if (is_callable($patternselect) && !is_string($patternselect)) { return call_user_func($patternselect, $address); } //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 @@ -1501,12 +1523,7 @@ class PHPMailer && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { - trigger_error( - 'Your version of PHP is affected by a bug that may result in corrupted messages.' . - ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . - ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', - E_USER_WARNING - ); + trigger_error($this->lang('buggy_php'), E_USER_WARNING); } try { @@ -1680,16 +1697,11 @@ class PHPMailer //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html //Example problem: https://www.drupal.org/node/1057954 - //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. - if ('' === $this->Sender) { - $this->Sender = $this->From; - } if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) { //PHP config has a sender address we can use $this->Sender = ini_get('sendmail_from'); } //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. - //But sendmail requires this param, so fail without it if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) { if ($this->Mailer === 'qmail') { $sendmailFmt = '%s -f%s'; @@ -1697,8 +1709,12 @@ class PHPMailer $sendmailFmt = '%s -oi -f%s -t'; } } else { - $this->edebug('Sender address unusable or missing: ' . $this->Sender); - return false; + //allow sendmail to choose a default envelope sender. It may + //seem preferable to force it to use the From header as with + //SMTP, but that introduces new problems (see + //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and + //it has historically worked this way. + $sendmailFmt = '%s -oi -t'; } $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); @@ -1718,9 +1734,10 @@ class PHPMailer fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); + $addrinfo = static::parseAddresses($toAddr, true, $this->charSet); $this->doCallback( ($result === 0), - [$toAddr], + [[$addrinfo['address'], $addrinfo['name']]], $this->cc, $this->bcc, $this->Subject, @@ -1807,7 +1824,8 @@ class PHPMailer */ protected static function isPermittedPath($path) { - return !preg_match('#^[a-z]+://#i', $path); + //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1 + return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path); } /** @@ -1819,12 +1837,15 @@ class PHPMailer */ protected static function fileIsAccessible($path) { + if (!static::isPermittedPath($path)) { + return false; + } $readable = file_exists($path); //If not a UNC path (expected to start with \\), check read permission, see #2069 if (strpos($path, '\\\\') !== 0) { $readable = $readable && is_readable($path); } - return static::isPermittedPath($path) && $readable; + return $readable; } /** @@ -1858,9 +1879,6 @@ class PHPMailer //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html //Example problem: https://www.drupal.org/node/1057954 //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. - if ('' === $this->Sender) { - $this->Sender = $this->From; - } if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) { //PHP config has a sender address we can use $this->Sender = ini_get('sendmail_from'); @@ -1876,7 +1894,17 @@ class PHPMailer if ($this->SingleTo && count($toArr) > 1) { foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); - $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + $addrinfo = static::parseAddresses($toAddr, true, $this->charSet); + $this->doCallback( + $result, + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); } } else { $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); @@ -1965,7 +1993,7 @@ class PHPMailer $isSent = true; } - $callbacks[] = ['issent' => $isSent, 'to' => $to[0]]; + $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]]; } } @@ -1986,7 +2014,7 @@ class PHPMailer foreach ($callbacks as $cb) { $this->doCallback( $cb['issent'], - [$cb['to']], + [[$cb['to'], $cb['name']]], [], [], $this->Subject, @@ -2163,13 +2191,15 @@ class PHPMailer /** * Set the language for error messages. - * Returns false if it cannot load the language file. * 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) + * Optionally, the language code can be enhanced with a 4-character + * script annotation and/or a 2-character country annotation. + * @param string $lang_path Path to the language file directory, with trailing separator (slash).D + * Do not set this from user input! * - * @return bool + * @return bool Returns true if the requested language was loaded, false otherwise. */ public function setLanguage($langcode = 'en', $lang_path = '') { @@ -2192,51 +2222,102 @@ class PHPMailer //Define full set of translatable strings in English $PHPMAILER_LANG = [ 'authenticate' => 'SMTP Error: Could not authenticate.', + 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', 'data_not_accepted' => 'SMTP Error: data not accepted.', 'empty_message' => 'Message body empty', 'encoding' => 'Unknown encoding: ', 'execute' => 'Could not execute: ', + 'extension_missing' => 'Extension missing: ', 'file_access' => 'Could not access file: ', 'file_open' => 'File Error: Could not open file: ', 'from_failed' => 'The following From address failed: ', 'instantiate' => 'Could not instantiate mail function.', 'invalid_address' => 'Invalid address: ', + 'invalid_header' => 'Invalid header name or value', 'invalid_hostentry' => 'Invalid hostentry: ', 'invalid_host' => 'Invalid host: ', 'mailer_not_supported' => ' mailer is not supported.', 'provide_address' => 'You must provide at least one recipient email address.', 'recipients_failed' => 'SMTP Error: The following recipients failed: ', 'signing' => 'Signing Error: ', + 'smtp_code' => 'SMTP code: ', + 'smtp_code_ex' => 'Additional SMTP info: ', 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_detail' => 'Detail: ', 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', - 'extension_missing' => 'Extension missing: ', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; } + //Validate $langcode - if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $foundlang = true; + $langcode = strtolower($langcode); + if ( + !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches) + && $langcode !== 'en' + ) { + $foundlang = false; $langcode = 'en'; } - $foundlang = true; - $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + //There is no English translation file if ('en' !== $langcode) { - //Make sure language file path is readable - if (!static::fileIsAccessible($lang_file)) { + $langcodes = []; + if (!empty($matches['script']) && !empty($matches['country'])) { + $langcodes[] = $matches['lang'] . $matches['script'] . $matches['country']; + } + if (!empty($matches['country'])) { + $langcodes[] = $matches['lang'] . $matches['country']; + } + if (!empty($matches['script'])) { + $langcodes[] = $matches['lang'] . $matches['script']; + } + $langcodes[] = $matches['lang']; + + //Try and find a readable language file for the requested language. + $foundFile = false; + foreach ($langcodes as $code) { + $lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php'; + if (static::fileIsAccessible($lang_file)) { + $foundFile = true; + break; + } + } + + if ($foundFile === false) { $foundlang = false; } else { - //Overwrite language-specific strings. - //This way we'll never have missing translation keys. - $foundlang = include $lang_file; + $lines = file($lang_file); + foreach ($lines as $line) { + //Translation file lines look like this: + //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.'; + //These files are parsed as text and not PHP so as to avoid the possibility of code injection + //See https://blog.stevenlevithan.com/archives/match-quoted-string + $matches = []; + if ( + preg_match( + '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/', + $line, + $matches + ) && + //Ignore unknown translation keys + array_key_exists($matches[1], $PHPMAILER_LANG) + ) { + //Overwrite language-specific strings so we'll never have missing translation keys. + $PHPMAILER_LANG[$matches[1]] = (string)$matches[3]; + } + } } } $this->language = $PHPMAILER_LANG; - return (bool) $foundlang; //Returns false if language not found + return $foundlang; //Returns false if language not found } /** @@ -2246,6 +2327,10 @@ class PHPMailer */ public function getTranslations() { + if (empty($this->language)) { + $this->setLanguage(); // Set the default language. + } + return $this->language; } @@ -2514,7 +2599,17 @@ class PHPMailer //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 //https://tools.ietf.org/html/rfc5322#section-3.6.4 - if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) { + if ( + '' !== $this->MessageID && + preg_match( + '/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' . + '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' . + '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' . + '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' . + '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di', + $this->MessageID + ) + ) { $this->lastMessageID = $this->MessageID; } else { $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); @@ -3898,13 +3993,13 @@ class PHPMailer if (!empty($lasterror['error'])) { $msg .= $this->lang('smtp_error') . $lasterror['error']; if (!empty($lasterror['detail'])) { - $msg .= ' Detail: ' . $lasterror['detail']; + $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail']; } if (!empty($lasterror['smtp_code'])) { - $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code']; } if (!empty($lasterror['smtp_code_ex'])) { - $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex']; } } } @@ -3965,7 +4060,7 @@ class PHPMailer empty($host) || !is_string($host) || strlen($host) > 256 - || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $host) + || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+\])$/', $host) ) { return false; } @@ -4042,11 +4137,11 @@ class PHPMailer list($name, $value) = explode(':', $name, 2); } $name = trim($name); - $value = trim($value); + $value = (null === $value) ? '' : trim($value); //Ensure name is not empty, and that neither name nor value contain line breaks if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception('Invalid header name or value'); + throw new Exception($this->lang('invalid_header')); } return false; @@ -4200,7 +4295,8 @@ class PHPMailer * * @param string $html The HTML text to convert * @param bool|callable $advanced Any boolean value to use the internal converter, - * or provide your own callable for custom conversion + * or provide your own callable for custom conversion. + * *Never* pass user-supplied data into this parameter * * @return string */ diff --git a/lib/PHPMailer/PHPMailer/SMTP.php b/lib/PHPMailer/PHPMailer/SMTP.php index 68f3aeccc..0cea1e864 100644 --- a/lib/PHPMailer/PHPMailer/SMTP.php +++ b/lib/PHPMailer/PHPMailer/SMTP.php @@ -35,7 +35,7 @@ class SMTP * * @var string */ - const VERSION = '6.3.0'; + const VERSION = '6.5.1'; /** * SMTP line break constant. @@ -186,6 +186,7 @@ class SMTP 'Amazon_SES' => '/[\d]{3} Ok (.*)/', 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', + 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', ]; /** @@ -553,6 +554,8 @@ class SMTP } //Send encoded username and password if ( + //Format from https://tools.ietf.org/html/rfc4616#section-2 + //We skip the first field (it's forgery), so the string starts with a null byte !$this->sendCommand( 'User & Password', base64_encode("\0" . $username . "\0" . $password), |
