wp/wp-includes/PHPMailer/PHPMailer.php
changeset 18 be944660c56a
parent 16 a86126ab1dd4
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
     1 <?php
     1 <?php
       
     2 
     2 /**
     3 /**
     3  * PHPMailer - PHP email creation and transport class.
     4  * PHPMailer - PHP email creation and transport class.
     4  * PHP Version 5.5.
     5  * PHP Version 5.5.
     5  *
     6  *
     6  * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
     7  * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
     7  *
     8  *
     8  * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
     9  * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
     9  * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
    10  * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
    10  * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
    11  * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
    11  * @author    Brent R. Matzelle (original founder)
    12  * @author    Brent R. Matzelle (original founder)
    12  * @copyright 2012 - 2019 Marcus Bointon
    13  * @copyright 2012 - 2020 Marcus Bointon
    13  * @copyright 2010 - 2012 Jim Jagielski
    14  * @copyright 2010 - 2012 Jim Jagielski
    14  * @copyright 2004 - 2009 Andy Prevost
    15  * @copyright 2004 - 2009 Andy Prevost
    15  * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
    16  * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
    16  * @note      This program is distributed in the hope that it will be useful - WITHOUT
    17  * @note      This program is distributed in the hope that it will be useful - WITHOUT
    17  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    18  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   386 
   387 
   387     /**
   388     /**
   388      * SMTP class debug output mode.
   389      * SMTP class debug output mode.
   389      * Debug output level.
   390      * Debug output level.
   390      * Options:
   391      * Options:
   391      * * SMTP::DEBUG_OFF: No output
   392      * @see SMTP::DEBUG_OFF: No output
   392      * * SMTP::DEBUG_CLIENT: Client messages
   393      * @see SMTP::DEBUG_CLIENT: Client messages
   393      * * SMTP::DEBUG_SERVER: Client and server messages
   394      * @see SMTP::DEBUG_SERVER: Client and server messages
   394      * * SMTP::DEBUG_CONNECTION: As SERVER plus connection status
   395      * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
   395      * * SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
   396      * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
   396      *
   397      *
   397      * @see SMTP::$do_debug
   398      * @see SMTP::$do_debug
   398      *
   399      *
   399      * @var int
   400      * @var int
   400      */
   401      */
   425      * @var string|callable|\Psr\Log\LoggerInterface
   426      * @var string|callable|\Psr\Log\LoggerInterface
   426      */
   427      */
   427     public $Debugoutput = 'echo';
   428     public $Debugoutput = 'echo';
   428 
   429 
   429     /**
   430     /**
   430      * Whether to keep SMTP connection open after each message.
   431      * Whether to keep the SMTP connection open after each message.
   431      * If this is set to true then to close the connection
   432      * If this is set to true then the connection will remain open after a send,
   432      * requires an explicit call to smtpClose().
   433      * and closing the connection will require an explicit call to smtpClose().
       
   434      * It's a good idea to use this if you are sending multiple messages as it reduces overhead.
       
   435      * See the mailing list example for how to use it.
   433      *
   436      *
   434      * @var bool
   437      * @var bool
   435      */
   438      */
   436     public $SMTPKeepAlive = false;
   439     public $SMTPKeepAlive = false;
   437 
   440 
   439      * Whether to split multiple to addresses into multiple messages
   442      * Whether to split multiple to addresses into multiple messages
   440      * or send them all in one message.
   443      * or send them all in one message.
   441      * Only supported in `mail` and `sendmail` transports, not in SMTP.
   444      * Only supported in `mail` and `sendmail` transports, not in SMTP.
   442      *
   445      *
   443      * @var bool
   446      * @var bool
       
   447      *
       
   448      * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
   444      */
   449      */
   445     public $SingleTo = false;
   450     public $SingleTo = false;
   446 
   451 
   447     /**
   452     /**
   448      * Storage for addresses when SingleTo is enabled.
   453      * Storage for addresses when SingleTo is enabled.
   743     /**
   748     /**
   744      * The PHPMailer Version number.
   749      * The PHPMailer Version number.
   745      *
   750      *
   746      * @var string
   751      * @var string
   747      */
   752      */
   748     const VERSION = '6.1.6';
   753     const VERSION = '6.5.0';
   749 
   754 
   750     /**
   755     /**
   751      * Error severity: message only, continue processing.
   756      * Error severity: message only, continue processing.
   752      *
   757      *
   753      * @var int
   758      * @var int
   857             $subject = $this->secureHeader($subject);
   862             $subject = $this->secureHeader($subject);
   858         } else {
   863         } else {
   859             $subject = $this->encodeHeader($this->secureHeader($subject));
   864             $subject = $this->encodeHeader($this->secureHeader($subject));
   860         }
   865         }
   861         //Calling mail() with null params breaks
   866         //Calling mail() with null params breaks
       
   867         $this->edebug('Sending with mail()');
       
   868         $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
       
   869         $this->edebug("Envelope sender: {$this->Sender}");
       
   870         $this->edebug("To: {$to}");
       
   871         $this->edebug("Subject: {$subject}");
       
   872         $this->edebug("Headers: {$header}");
   862         if (!$this->UseSendmailOptions || null === $params) {
   873         if (!$this->UseSendmailOptions || null === $params) {
   863             $result = @mail($to, $subject, $body, $header);
   874             $result = @mail($to, $subject, $body, $header);
   864         } else {
   875         } else {
       
   876             $this->edebug("Additional params: {$params}");
   865             $result = @mail($to, $subject, $body, $header, $params);
   877             $result = @mail($to, $subject, $body, $header, $params);
   866         }
   878         }
   867 
   879         $this->edebug('Result: ' . ($result ? 'true' : 'false'));
   868         return $result;
   880         return $result;
   869     }
   881     }
   870 
   882 
   871     /**
   883     /**
   872      * Output debugging info via user-defined method.
   884      * Output debugging info via a user-defined method.
   873      * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
   885      * Only generates output if debug output is enabled.
   874      *
   886      *
   875      * @see PHPMailer::$Debugoutput
   887      * @see PHPMailer::$Debugoutput
   876      * @see PHPMailer::$SMTPDebug
   888      * @see PHPMailer::$SMTPDebug
   877      *
   889      *
   878      * @param string $str
   890      * @param string $str
   895             return;
   907             return;
   896         }
   908         }
   897         switch ($this->Debugoutput) {
   909         switch ($this->Debugoutput) {
   898             case 'error_log':
   910             case 'error_log':
   899                 //Don't output, just log
   911                 //Don't output, just log
       
   912                 /** @noinspection ForgottenDebugOutputInspection */
   900                 error_log($str);
   913                 error_log($str);
   901                 break;
   914                 break;
   902             case 'html':
   915             case 'html':
   903                 //Cleans up output a bit for a better looking, HTML-safe output
   916                 //Cleans up output a bit for a better looking, HTML-safe output
   904                 echo htmlentities(
   917                 echo htmlentities(
  1064     {
  1077     {
  1065         $address = trim($address);
  1078         $address = trim($address);
  1066         $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
  1079         $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
  1067         $pos = strrpos($address, '@');
  1080         $pos = strrpos($address, '@');
  1068         if (false === $pos) {
  1081         if (false === $pos) {
  1069             // At-sign is missing.
  1082             //At-sign is missing.
  1070             $error_message = sprintf(
  1083             $error_message = sprintf(
  1071                 '%s (%s): %s',
  1084                 '%s (%s): %s',
  1072                 $this->lang('invalid_address'),
  1085                 $this->lang('invalid_address'),
  1073                 $kind,
  1086                 $kind,
  1074                 $address
  1087                 $address
  1080             }
  1093             }
  1081 
  1094 
  1082             return false;
  1095             return false;
  1083         }
  1096         }
  1084         $params = [$kind, $address, $name];
  1097         $params = [$kind, $address, $name];
  1085         // Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
  1098         //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
  1086         if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
  1099         if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
  1087             if ('Reply-To' !== $kind) {
  1100             if ('Reply-To' !== $kind) {
  1088                 if (!array_key_exists($address, $this->RecipientsQueue)) {
  1101                 if (!array_key_exists($address, $this->RecipientsQueue)) {
  1089                     $this->RecipientsQueue[$address] = $params;
  1102                     $this->RecipientsQueue[$address] = $params;
  1090 
  1103 
  1097             }
  1110             }
  1098 
  1111 
  1099             return false;
  1112             return false;
  1100         }
  1113         }
  1101 
  1114 
  1102         // Immediately add standard addresses without IDN.
  1115         //Immediately add standard addresses without IDN.
  1103         return call_user_func_array([$this, 'addAnAddress'], $params);
  1116         return call_user_func_array([$this, 'addAnAddress'], $params);
  1104     }
  1117     }
  1105 
  1118 
  1106     /**
  1119     /**
  1107      * Add an address to one of the recipient arrays or to the ReplyTo array.
  1120      * Add an address to one of the recipient arrays or to the ReplyTo array.
  1180         $addresses = [];
  1193         $addresses = [];
  1181         if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
  1194         if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
  1182             //Use this built-in parser if it's available
  1195             //Use this built-in parser if it's available
  1183             $list = imap_rfc822_parse_adrlist($addrstr, '');
  1196             $list = imap_rfc822_parse_adrlist($addrstr, '');
  1184             foreach ($list as $address) {
  1197             foreach ($list as $address) {
  1185                 if (('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress(
  1198                 if (
  1186                     $address->mailbox . '@' . $address->host
  1199                     ('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress(
  1187                 )) {
  1200                         $address->mailbox . '@' . $address->host
       
  1201                     )
       
  1202                 ) {
       
  1203                     //Decode the name part if it's present and encoded
       
  1204                     if (
       
  1205                         property_exists($address, 'personal') &&
       
  1206                         extension_loaded('mbstring') &&
       
  1207                         preg_match('/^=\?.*\?=$/', $address->personal)
       
  1208                     ) {
       
  1209                         $address->personal = mb_decode_mimeheader($address->personal);
       
  1210                     }
       
  1211 
  1188                     $addresses[] = [
  1212                     $addresses[] = [
  1189                         'name' => (property_exists($address, 'personal') ? $address->personal : ''),
  1213                         'name' => (property_exists($address, 'personal') ? $address->personal : ''),
  1190                         'address' => $address->mailbox . '@' . $address->host,
  1214                         'address' => $address->mailbox . '@' . $address->host,
  1191                     ];
  1215                     ];
  1192                 }
  1216                 }
  1206                         ];
  1230                         ];
  1207                     }
  1231                     }
  1208                 } else {
  1232                 } else {
  1209                     list($name, $email) = explode('<', $address);
  1233                     list($name, $email) = explode('<', $address);
  1210                     $email = trim(str_replace('>', '', $email));
  1234                     $email = trim(str_replace('>', '', $email));
       
  1235                     $name = trim($name);
  1211                     if (static::validateAddress($email)) {
  1236                     if (static::validateAddress($email)) {
       
  1237                         //If this name is encoded, decode it
       
  1238                         if (preg_match('/^=\?.*\?=$/', $name)) {
       
  1239                             $name = mb_decode_mimeheader($name);
       
  1240                         }
  1212                         $addresses[] = [
  1241                         $addresses[] = [
  1213                             'name' => trim(str_replace(['"', "'"], '', $name)),
  1242                             //Remove any surrounding quotes and spaces from the name
       
  1243                             'name' => trim($name, '\'" '),
  1214                             'address' => $email,
  1244                             'address' => $email,
  1215                         ];
  1245                         ];
  1216                     }
  1246                     }
  1217                 }
  1247                 }
  1218             }
  1248             }
  1234      */
  1264      */
  1235     public function setFrom($address, $name = '', $auto = true)
  1265     public function setFrom($address, $name = '', $auto = true)
  1236     {
  1266     {
  1237         $address = trim($address);
  1267         $address = trim($address);
  1238         $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
  1268         $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
  1239         // Don't validate now addresses with IDN. Will be done in send().
  1269         //Don't validate now addresses with IDN. Will be done in send().
  1240         $pos = strrpos($address, '@');
  1270         $pos = strrpos($address, '@');
  1241         if ((false === $pos)
  1271         if (
       
  1272             (false === $pos)
  1242             || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
  1273             || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
  1243             && !static::validateAddress($address))
  1274             && !static::validateAddress($address))
  1244         ) {
  1275         ) {
  1245             $error_message = sprintf(
  1276             $error_message = sprintf(
  1246                 '%s (From): %s',
  1277                 '%s (From): %s',
  1304     public static function validateAddress($address, $patternselect = null)
  1335     public static function validateAddress($address, $patternselect = null)
  1305     {
  1336     {
  1306         if (null === $patternselect) {
  1337         if (null === $patternselect) {
  1307             $patternselect = static::$validator;
  1338             $patternselect = static::$validator;
  1308         }
  1339         }
  1309         if (is_callable($patternselect)) {
  1340         //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
  1310             return $patternselect($address);
  1341         if (is_callable($patternselect) && !is_string($patternselect)) {
       
  1342             return call_user_func($patternselect, $address);
  1311         }
  1343         }
  1312         //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
  1344         //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
  1313         if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
  1345         if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
  1314             return false;
  1346             return false;
  1315         }
  1347         }
  1346                 );
  1378                 );
  1347             case 'html5':
  1379             case 'html5':
  1348                 /*
  1380                 /*
  1349                  * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
  1381                  * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
  1350                  *
  1382                  *
  1351                  * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email)
  1383                  * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
  1352                  */
  1384                  */
  1353                 return (bool) preg_match(
  1385                 return (bool) preg_match(
  1354                     '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
  1386                     '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
  1355                     '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
  1387                     '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
  1356                     $address
  1388                     $address
  1386      *
  1418      *
  1387      * @return string The encoded address in ASCII form
  1419      * @return string The encoded address in ASCII form
  1388      */
  1420      */
  1389     public function punyencodeAddress($address)
  1421     public function punyencodeAddress($address)
  1390     {
  1422     {
  1391         // Verify we have required functions, CharSet, and at-sign.
  1423         //Verify we have required functions, CharSet, and at-sign.
  1392         $pos = strrpos($address, '@');
  1424         $pos = strrpos($address, '@');
  1393         if (!empty($this->CharSet) &&
  1425         if (
       
  1426             !empty($this->CharSet) &&
  1394             false !== $pos &&
  1427             false !== $pos &&
  1395             static::idnSupported()
  1428             static::idnSupported()
  1396         ) {
  1429         ) {
  1397             $domain = substr($address, ++$pos);
  1430             $domain = substr($address, ++$pos);
  1398             // Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
  1431             //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
  1399             if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
  1432             if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
  1400                 $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet);
  1433                 //Convert the domain from whatever charset it's in to UTF-8
       
  1434                 $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
  1401                 //Ignore IDE complaints about this line - method signature changed in PHP 5.4
  1435                 //Ignore IDE complaints about this line - method signature changed in PHP 5.4
  1402                 $errorcode = 0;
  1436                 $errorcode = 0;
  1403                 if (defined('INTL_IDNA_VARIANT_UTS46')) {
  1437                 if (defined('INTL_IDNA_VARIANT_UTS46')) {
  1404                     // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
  1438                     //Use the current punycode standard (appeared in PHP 7.2)
  1405                     $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46);
  1439                     $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_UTS46);
  1406                 } elseif (defined('INTL_IDNA_VARIANT_2003')) {
  1440                 } elseif (defined('INTL_IDNA_VARIANT_2003')) {
       
  1441                     //Fall back to this old, deprecated/removed encoding
  1407                     // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
  1442                     // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated
  1408                     $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_2003);
  1443                     $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
  1409                 } else {
  1444                 } else {
       
  1445                     //Fall back to a default we don't know about
  1410                     // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
  1446                     // phpcs:ignore PHPCompatibility.ParameterValues.NewIDNVariantDefault.NotSet
  1411                     $punycode = idn_to_ascii($domain, $errorcode);
  1447                     $punycode = idn_to_ascii($domain, $errorcode);
  1412                 }
  1448                 }
  1413                 if (false !== $punycode) {
  1449                 if (false !== $punycode) {
  1414                     return substr($address, 0, $pos) . $punycode;
  1450                     return substr($address, 0, $pos) . $punycode;
  1453      *
  1489      *
  1454      * @return bool
  1490      * @return bool
  1455      */
  1491      */
  1456     public function preSend()
  1492     public function preSend()
  1457     {
  1493     {
  1458         if ('smtp' === $this->Mailer
  1494         if (
  1459             || ('mail' === $this->Mailer && stripos(PHP_OS, 'WIN') === 0)
  1495             'smtp' === $this->Mailer
       
  1496             || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
  1460         ) {
  1497         ) {
  1461             //SMTP mandates RFC-compliant line endings
  1498             //SMTP mandates RFC-compliant line endings
  1462             //and it's also used with mail() on Windows
  1499             //and it's also used with mail() on Windows
  1463             static::setLE(self::CRLF);
  1500             static::setLE(self::CRLF);
  1464         } else {
  1501         } else {
  1465             //Maintain backward compatibility with legacy Linux command line mailers
  1502             //Maintain backward compatibility with legacy Linux command line mailers
  1466             static::setLE(PHP_EOL);
  1503             static::setLE(PHP_EOL);
  1467         }
  1504         }
  1468         //Check for buggy PHP versions that add a header with an incorrect line break
  1505         //Check for buggy PHP versions that add a header with an incorrect line break
  1469         if ('mail' === $this->Mailer
  1506         if (
  1470             && ((PHP_VERSION_ID >= 70000 && PHP_VERSION_ID < 70017)
  1507             'mail' === $this->Mailer
  1471                 || (PHP_VERSION_ID >= 70100 && PHP_VERSION_ID < 70103))
  1508             && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
       
  1509                 || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
  1472             && ini_get('mail.add_x_header') === '1'
  1510             && ini_get('mail.add_x_header') === '1'
  1473             && stripos(PHP_OS, 'WIN') === 0
  1511             && stripos(PHP_OS, 'WIN') === 0
  1474         ) {
  1512         ) {
  1475             trigger_error(
  1513             trigger_error(
  1476                 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
  1514                 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
  1479                 E_USER_WARNING
  1517                 E_USER_WARNING
  1480             );
  1518             );
  1481         }
  1519         }
  1482 
  1520 
  1483         try {
  1521         try {
  1484             $this->error_count = 0; // Reset errors
  1522             $this->error_count = 0; //Reset errors
  1485             $this->mailHeader = '';
  1523             $this->mailHeader = '';
  1486 
  1524 
  1487             // Dequeue recipient and Reply-To addresses with IDN
  1525             //Dequeue recipient and Reply-To addresses with IDN
  1488             foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
  1526             foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
  1489                 $params[1] = $this->punyencodeAddress($params[1]);
  1527                 $params[1] = $this->punyencodeAddress($params[1]);
  1490                 call_user_func_array([$this, 'addAnAddress'], $params);
  1528                 call_user_func_array([$this, 'addAnAddress'], $params);
  1491             }
  1529             }
  1492             if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
  1530             if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
  1493                 throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
  1531                 throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
  1494             }
  1532             }
  1495 
  1533 
  1496             // Validate From, Sender, and ConfirmReadingTo addresses
  1534             //Validate From, Sender, and ConfirmReadingTo addresses
  1497             foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
  1535             foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
  1498                 $this->$address_kind = trim($this->$address_kind);
  1536                 $this->$address_kind = trim($this->$address_kind);
  1499                 if (empty($this->$address_kind)) {
  1537                 if (empty($this->$address_kind)) {
  1500                     continue;
  1538                     continue;
  1501                 }
  1539                 }
  1515 
  1553 
  1516                     return false;
  1554                     return false;
  1517                 }
  1555                 }
  1518             }
  1556             }
  1519 
  1557 
  1520             // Set whether the message is multipart/alternative
  1558             //Set whether the message is multipart/alternative
  1521             if ($this->alternativeExists()) {
  1559             if ($this->alternativeExists()) {
  1522                 $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
  1560                 $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
  1523             }
  1561             }
  1524 
  1562 
  1525             $this->setMessageType();
  1563             $this->setMessageType();
  1526             // Refuse to send an empty message unless we are specifically allowing it
  1564             //Refuse to send an empty message unless we are specifically allowing it
  1527             if (!$this->AllowEmpty && empty($this->Body)) {
  1565             if (!$this->AllowEmpty && empty($this->Body)) {
  1528                 throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
  1566                 throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
  1529             }
  1567             }
  1530 
  1568 
  1531             //Trim subject consistently
  1569             //Trim subject consistently
  1532             $this->Subject = trim($this->Subject);
  1570             $this->Subject = trim($this->Subject);
  1533             // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
  1571             //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
  1534             $this->MIMEHeader = '';
  1572             $this->MIMEHeader = '';
  1535             $this->MIMEBody = $this->createBody();
  1573             $this->MIMEBody = $this->createBody();
  1536             // createBody may have added some headers, so retain them
  1574             //createBody may have added some headers, so retain them
  1537             $tempheaders = $this->MIMEHeader;
  1575             $tempheaders = $this->MIMEHeader;
  1538             $this->MIMEHeader = $this->createHeader();
  1576             $this->MIMEHeader = $this->createHeader();
  1539             $this->MIMEHeader .= $tempheaders;
  1577             $this->MIMEHeader .= $tempheaders;
  1540 
  1578 
  1541             // To capture the complete message when using mail(), create
  1579             //To capture the complete message when using mail(), create
  1542             // an extra header list which createHeader() doesn't fold in
  1580             //an extra header list which createHeader() doesn't fold in
  1543             if ('mail' === $this->Mailer) {
  1581             if ('mail' === $this->Mailer) {
  1544                 if (count($this->to) > 0) {
  1582                 if (count($this->to) > 0) {
  1545                     $this->mailHeader .= $this->addrAppend('To', $this->to);
  1583                     $this->mailHeader .= $this->addrAppend('To', $this->to);
  1546                 } else {
  1584                 } else {
  1547                     $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
  1585                     $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
  1550                     'Subject',
  1588                     'Subject',
  1551                     $this->encodeHeader($this->secureHeader($this->Subject))
  1589                     $this->encodeHeader($this->secureHeader($this->Subject))
  1552                 );
  1590                 );
  1553             }
  1591             }
  1554 
  1592 
  1555             // Sign with DKIM if enabled
  1593             //Sign with DKIM if enabled
  1556             if (!empty($this->DKIM_domain)
  1594             if (
       
  1595                 !empty($this->DKIM_domain)
  1557                 && !empty($this->DKIM_selector)
  1596                 && !empty($this->DKIM_selector)
  1558                 && (!empty($this->DKIM_private_string)
  1597                 && (!empty($this->DKIM_private_string)
  1559                     || (!empty($this->DKIM_private)
  1598                     || (!empty($this->DKIM_private)
  1560                         && static::isPermittedPath($this->DKIM_private)
  1599                         && static::isPermittedPath($this->DKIM_private)
  1561                         && file_exists($this->DKIM_private)
  1600                         && file_exists($this->DKIM_private)
  1590      * @return bool
  1629      * @return bool
  1591      */
  1630      */
  1592     public function postSend()
  1631     public function postSend()
  1593     {
  1632     {
  1594         try {
  1633         try {
  1595             // Choose the mailer and send through it
  1634             //Choose the mailer and send through it
  1596             switch ($this->Mailer) {
  1635             switch ($this->Mailer) {
  1597                 case 'sendmail':
  1636                 case 'sendmail':
  1598                 case 'qmail':
  1637                 case 'qmail':
  1599                     return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
  1638                     return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
  1600                 case 'smtp':
  1639                 case 'smtp':
  1608                     }
  1647                     }
  1609 
  1648 
  1610                     return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
  1649                     return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
  1611             }
  1650             }
  1612         } catch (Exception $exc) {
  1651         } catch (Exception $exc) {
       
  1652             if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true) {
       
  1653                 $this->smtp->reset();
       
  1654             }
  1613             $this->setError($exc->getMessage());
  1655             $this->setError($exc->getMessage());
  1614             $this->edebug($exc->getMessage());
  1656             $this->edebug($exc->getMessage());
  1615             if ($this->exceptions) {
  1657             if ($this->exceptions) {
  1616                 throw $exc;
  1658                 throw $exc;
  1617             }
  1659             }
  1632      *
  1674      *
  1633      * @return bool
  1675      * @return bool
  1634      */
  1676      */
  1635     protected function sendmailSend($header, $body)
  1677     protected function sendmailSend($header, $body)
  1636     {
  1678     {
       
  1679         if ($this->Mailer === 'qmail') {
       
  1680             $this->edebug('Sending with qmail');
       
  1681         } else {
       
  1682             $this->edebug('Sending with sendmail');
       
  1683         }
  1637         $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
  1684         $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
  1638 
  1685         //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
  1639         // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
  1686         //A space after `-f` is optional, but there is a long history of its presence
  1640         if (!empty($this->Sender) && self::isShellSafe($this->Sender)) {
  1687         //causing problems, so we don't use one
  1641             if ('qmail' === $this->Mailer) {
  1688         //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
       
  1689         //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
       
  1690         //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
       
  1691         //Example problem: https://www.drupal.org/node/1057954
       
  1692         if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) {
       
  1693             //PHP config has a sender address we can use
       
  1694             $this->Sender = ini_get('sendmail_from');
       
  1695         }
       
  1696         //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
       
  1697         if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
       
  1698             if ($this->Mailer === 'qmail') {
  1642                 $sendmailFmt = '%s -f%s';
  1699                 $sendmailFmt = '%s -f%s';
  1643             } else {
  1700             } else {
  1644                 $sendmailFmt = '%s -oi -f%s -t';
  1701                 $sendmailFmt = '%s -oi -f%s -t';
  1645             }
  1702             }
  1646         } elseif ('qmail' === $this->Mailer) {
       
  1647             $sendmailFmt = '%s';
       
  1648         } else {
  1703         } else {
       
  1704             //allow sendmail to choose a default envelope sender. It may
       
  1705             //seem preferable to force it to use the From header as with
       
  1706             //SMTP, but that introduces new problems (see
       
  1707             //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
       
  1708             //it has historically worked this way.
  1649             $sendmailFmt = '%s -oi -t';
  1709             $sendmailFmt = '%s -oi -t';
  1650         }
  1710         }
  1651 
  1711 
  1652         $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
  1712         $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
       
  1713         $this->edebug('Sendmail path: ' . $this->Sendmail);
       
  1714         $this->edebug('Sendmail command: ' . $sendmail);
       
  1715         $this->edebug('Envelope sender: ' . $this->Sender);
       
  1716         $this->edebug("Headers: {$header}");
  1653 
  1717 
  1654         if ($this->SingleTo) {
  1718         if ($this->SingleTo) {
  1655             foreach ($this->SingleToArray as $toAddr) {
  1719             foreach ($this->SingleToArray as $toAddr) {
  1656                 $mail = @popen($sendmail, 'w');
  1720                 $mail = @popen($sendmail, 'w');
  1657                 if (!$mail) {
  1721                 if (!$mail) {
  1658                     throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
  1722                     throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
  1659                 }
  1723                 }
       
  1724                 $this->edebug("To: {$toAddr}");
  1660                 fwrite($mail, 'To: ' . $toAddr . "\n");
  1725                 fwrite($mail, 'To: ' . $toAddr . "\n");
  1661                 fwrite($mail, $header);
  1726                 fwrite($mail, $header);
  1662                 fwrite($mail, $body);
  1727                 fwrite($mail, $body);
  1663                 $result = pclose($mail);
  1728                 $result = pclose($mail);
       
  1729                 $addrinfo = static::parseAddresses($toAddr);
  1664                 $this->doCallback(
  1730                 $this->doCallback(
  1665                     ($result === 0),
  1731                     ($result === 0),
  1666                     [$toAddr],
  1732                     [[$addrinfo['address'], $addrinfo['name']]],
  1667                     $this->cc,
  1733                     $this->cc,
  1668                     $this->bcc,
  1734                     $this->bcc,
  1669                     $this->Subject,
  1735                     $this->Subject,
  1670                     $body,
  1736                     $body,
  1671                     $this->From,
  1737                     $this->From,
  1672                     []
  1738                     []
  1673                 );
  1739                 );
       
  1740                 $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
  1674                 if (0 !== $result) {
  1741                 if (0 !== $result) {
  1675                     throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
  1742                     throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
  1676                 }
  1743                 }
  1677             }
  1744             }
  1678         } else {
  1745         } else {
  1691                 $this->Subject,
  1758                 $this->Subject,
  1692                 $body,
  1759                 $body,
  1693                 $this->From,
  1760                 $this->From,
  1694                 []
  1761                 []
  1695             );
  1762             );
       
  1763             $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
  1696             if (0 !== $result) {
  1764             if (0 !== $result) {
  1697                 throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
  1765                 throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
  1698             }
  1766             }
  1699         }
  1767         }
  1700 
  1768 
  1711      *
  1779      *
  1712      * @return bool
  1780      * @return bool
  1713      */
  1781      */
  1714     protected static function isShellSafe($string)
  1782     protected static function isShellSafe($string)
  1715     {
  1783     {
  1716         // Future-proof
  1784         //Future-proof
  1717         if (escapeshellcmd($string) !== $string
  1785         if (
       
  1786             escapeshellcmd($string) !== $string
  1718             || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
  1787             || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
  1719         ) {
  1788         ) {
  1720             return false;
  1789             return false;
  1721         }
  1790         }
  1722 
  1791 
  1723         $length = strlen($string);
  1792         $length = strlen($string);
  1724 
  1793 
  1725         for ($i = 0; $i < $length; ++$i) {
  1794         for ($i = 0; $i < $length; ++$i) {
  1726             $c = $string[$i];
  1795             $c = $string[$i];
  1727 
  1796 
  1728             // All other characters have a special meaning in at least one common shell, including = and +.
  1797             //All other characters have a special meaning in at least one common shell, including = and +.
  1729             // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
  1798             //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
  1730             // Note that this does permit non-Latin alphanumeric characters based on the current locale.
  1799             //Note that this does permit non-Latin alphanumeric characters based on the current locale.
  1731             if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
  1800             if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
  1732                 return false;
  1801                 return false;
  1733             }
  1802             }
  1734         }
  1803         }
  1735 
  1804 
  1745      *
  1814      *
  1746      * @return bool
  1815      * @return bool
  1747      */
  1816      */
  1748     protected static function isPermittedPath($path)
  1817     protected static function isPermittedPath($path)
  1749     {
  1818     {
  1750         return !preg_match('#^[a-z]+://#i', $path);
  1819         //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1
       
  1820         return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
       
  1821     }
       
  1822 
       
  1823     /**
       
  1824      * Check whether a file path is safe, accessible, and readable.
       
  1825      *
       
  1826      * @param string $path A relative or absolute path to a file
       
  1827      *
       
  1828      * @return bool
       
  1829      */
       
  1830     protected static function fileIsAccessible($path)
       
  1831     {
       
  1832         if (!static::isPermittedPath($path)) {
       
  1833             return false;
       
  1834         }
       
  1835         $readable = file_exists($path);
       
  1836         //If not a UNC path (expected to start with \\), check read permission, see #2069
       
  1837         if (strpos($path, '\\\\') !== 0) {
       
  1838             $readable = $readable && is_readable($path);
       
  1839         }
       
  1840         return  $readable;
  1751     }
  1841     }
  1752 
  1842 
  1753     /**
  1843     /**
  1754      * Send mail using the PHP mail() function.
  1844      * Send mail using the PHP mail() function.
  1755      *
  1845      *
  1778         //causing problems, so we don't use one
  1868         //causing problems, so we don't use one
  1779         //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
  1869         //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
  1780         //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
  1870         //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
  1781         //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
  1871         //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
  1782         //Example problem: https://www.drupal.org/node/1057954
  1872         //Example problem: https://www.drupal.org/node/1057954
  1783         // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
  1873         //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
  1784         if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
  1874         if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) {
  1785             $params = sprintf('-f%s', $this->Sender);
  1875             //PHP config has a sender address we can use
       
  1876             $this->Sender = ini_get('sendmail_from');
  1786         }
  1877         }
  1787         if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
  1878         if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
       
  1879             if (self::isShellSafe($this->Sender)) {
       
  1880                 $params = sprintf('-f%s', $this->Sender);
       
  1881             }
  1788             $old_from = ini_get('sendmail_from');
  1882             $old_from = ini_get('sendmail_from');
  1789             ini_set('sendmail_from', $this->Sender);
  1883             ini_set('sendmail_from', $this->Sender);
  1790         }
  1884         }
  1791         $result = false;
  1885         $result = false;
  1792         if ($this->SingleTo && count($toArr) > 1) {
  1886         if ($this->SingleTo && count($toArr) > 1) {
  1793             foreach ($toArr as $toAddr) {
  1887             foreach ($toArr as $toAddr) {
  1794                 $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
  1888                 $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
  1795                 $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
  1889                 $addrinfo = static::parseAddresses($toAddr);
       
  1890                 $this->doCallback(
       
  1891                     $result,
       
  1892                     [[$addrinfo['address'], $addrinfo['name']]],
       
  1893                     $this->cc,
       
  1894                     $this->bcc,
       
  1895                     $this->Subject,
       
  1896                     $body,
       
  1897                     $this->From,
       
  1898                     []
       
  1899                 );
  1796             }
  1900             }
  1797         } else {
  1901         } else {
  1798             $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
  1902             $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
  1799             $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
  1903             $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
  1800         }
  1904         }
  1868             $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
  1972             $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
  1869             throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
  1973             throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
  1870         }
  1974         }
  1871 
  1975 
  1872         $callbacks = [];
  1976         $callbacks = [];
  1873         // Attempt to send to all recipients
  1977         //Attempt to send to all recipients
  1874         foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
  1978         foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
  1875             foreach ($togroup as $to) {
  1979             foreach ($togroup as $to) {
  1876                 if (!$this->smtp->recipient($to[0], $this->dsn)) {
  1980                 if (!$this->smtp->recipient($to[0], $this->dsn)) {
  1877                     $error = $this->smtp->getError();
  1981                     $error = $this->smtp->getError();
  1878                     $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
  1982                     $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
  1879                     $isSent = false;
  1983                     $isSent = false;
  1880                 } else {
  1984                 } else {
  1881                     $isSent = true;
  1985                     $isSent = true;
  1882                 }
  1986                 }
  1883 
  1987 
  1884                 $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]];
  1988                 $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
  1885             }
  1989             }
  1886         }
  1990         }
  1887 
  1991 
  1888         // Only send the DATA command if we have viable recipients
  1992         //Only send the DATA command if we have viable recipients
  1889         if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
  1993         if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
  1890             throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
  1994             throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
  1891         }
  1995         }
  1892 
  1996 
  1893         $smtp_transaction_id = $this->smtp->getLastTransactionID();
  1997         $smtp_transaction_id = $this->smtp->getLastTransactionID();
  1900         }
  2004         }
  1901 
  2005 
  1902         foreach ($callbacks as $cb) {
  2006         foreach ($callbacks as $cb) {
  1903             $this->doCallback(
  2007             $this->doCallback(
  1904                 $cb['issent'],
  2008                 $cb['issent'],
  1905                 [$cb['to']],
  2009                 [[$cb['to'], $cb['name']]],
  1906                 [],
  2010                 [],
  1907                 [],
  2011                 [],
  1908                 $this->Subject,
  2012                 $this->Subject,
  1909                 $body,
  2013                 $body,
  1910                 $this->From,
  2014                 $this->From,
  1945         //If no options are provided, use whatever is set in the instance
  2049         //If no options are provided, use whatever is set in the instance
  1946         if (null === $options) {
  2050         if (null === $options) {
  1947             $options = $this->SMTPOptions;
  2051             $options = $this->SMTPOptions;
  1948         }
  2052         }
  1949 
  2053 
  1950         // Already connected?
  2054         //Already connected?
  1951         if ($this->smtp->connected()) {
  2055         if ($this->smtp->connected()) {
  1952             return true;
  2056             return true;
  1953         }
  2057         }
  1954 
  2058 
  1955         $this->smtp->setTimeout($this->Timeout);
  2059         $this->smtp->setTimeout($this->Timeout);
  1959         $hosts = explode(';', $this->Host);
  2063         $hosts = explode(';', $this->Host);
  1960         $lastexception = null;
  2064         $lastexception = null;
  1961 
  2065 
  1962         foreach ($hosts as $hostentry) {
  2066         foreach ($hosts as $hostentry) {
  1963             $hostinfo = [];
  2067             $hostinfo = [];
  1964             if (!preg_match(
  2068             if (
  1965                 '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
  2069                 !preg_match(
  1966                 trim($hostentry),
  2070                     '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
  1967                 $hostinfo
  2071                     trim($hostentry),
  1968             )) {
  2072                     $hostinfo
       
  2073                 )
       
  2074             ) {
  1969                 $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
  2075                 $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
  1970                 // Not a valid host entry
  2076                 //Not a valid host entry
  1971                 continue;
  2077                 continue;
  1972             }
  2078             }
  1973             // $hostinfo[1]: optional ssl or tls prefix
  2079             //$hostinfo[1]: optional ssl or tls prefix
  1974             // $hostinfo[2]: the hostname
  2080             //$hostinfo[2]: the hostname
  1975             // $hostinfo[3]: optional port number
  2081             //$hostinfo[3]: optional port number
  1976             // The host string prefix can temporarily override the current setting for SMTPSecure
  2082             //The host string prefix can temporarily override the current setting for SMTPSecure
  1977             // If it's not specified, the default value is used
  2083             //If it's not specified, the default value is used
  1978 
  2084 
  1979             //Check the host name is a valid name or IP address before trying to use it
  2085             //Check the host name is a valid name or IP address before trying to use it
  1980             if (!static::isValidHost($hostinfo[2])) {
  2086             if (!static::isValidHost($hostinfo[2])) {
  1981                 $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
  2087                 $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
  1982                 continue;
  2088                 continue;
  1984             $prefix = '';
  2090             $prefix = '';
  1985             $secure = $this->SMTPSecure;
  2091             $secure = $this->SMTPSecure;
  1986             $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
  2092             $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
  1987             if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
  2093             if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
  1988                 $prefix = 'ssl://';
  2094                 $prefix = 'ssl://';
  1989                 $tls = false; // Can't have SSL and TLS at the same time
  2095                 $tls = false; //Can't have SSL and TLS at the same time
  1990                 $secure = static::ENCRYPTION_SMTPS;
  2096                 $secure = static::ENCRYPTION_SMTPS;
  1991             } elseif ('tls' === $hostinfo[1]) {
  2097             } elseif ('tls' === $hostinfo[1]) {
  1992                 $tls = true;
  2098                 $tls = true;
  1993                 // tls doesn't use a prefix
  2099                 //TLS doesn't use a prefix
  1994                 $secure = static::ENCRYPTION_STARTTLS;
  2100                 $secure = static::ENCRYPTION_STARTTLS;
  1995             }
  2101             }
  1996             //Do we need the OpenSSL extension?
  2102             //Do we need the OpenSSL extension?
  1997             $sslext = defined('OPENSSL_ALGO_SHA256');
  2103             $sslext = defined('OPENSSL_ALGO_SHA256');
  1998             if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
  2104             if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
  2001                     throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
  2107                     throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
  2002                 }
  2108                 }
  2003             }
  2109             }
  2004             $host = $hostinfo[2];
  2110             $host = $hostinfo[2];
  2005             $port = $this->Port;
  2111             $port = $this->Port;
  2006             if (array_key_exists(3, $hostinfo) && is_numeric($hostinfo[3]) && $hostinfo[3] > 0 && $hostinfo[3] < 65536) {
  2112             if (
       
  2113                 array_key_exists(3, $hostinfo) &&
       
  2114                 is_numeric($hostinfo[3]) &&
       
  2115                 $hostinfo[3] > 0 &&
       
  2116                 $hostinfo[3] < 65536
       
  2117             ) {
  2007                 $port = (int) $hostinfo[3];
  2118                 $port = (int) $hostinfo[3];
  2008             }
  2119             }
  2009             if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
  2120             if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
  2010                 try {
  2121                 try {
  2011                     if ($this->Helo) {
  2122                     if ($this->Helo) {
  2013                     } else {
  2124                     } else {
  2014                         $hello = $this->serverHostname();
  2125                         $hello = $this->serverHostname();
  2015                     }
  2126                     }
  2016                     $this->smtp->hello($hello);
  2127                     $this->smtp->hello($hello);
  2017                     //Automatically enable TLS encryption if:
  2128                     //Automatically enable TLS encryption if:
  2018                     // * it's not disabled
  2129                     //* it's not disabled
  2019                     // * we have openssl extension
  2130                     //* we have openssl extension
  2020                     // * we are not already using SSL
  2131                     //* we are not already using SSL
  2021                     // * the server offers STARTTLS
  2132                     //* the server offers STARTTLS
  2022                     if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
  2133                     if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
  2023                         $tls = true;
  2134                         $tls = true;
  2024                     }
  2135                     }
  2025                     if ($tls) {
  2136                     if ($tls) {
  2026                         if (!$this->smtp->startTLS()) {
  2137                         if (!$this->smtp->startTLS()) {
  2027                             throw new Exception($this->lang('connect_host'));
  2138                             throw new Exception($this->lang('connect_host'));
  2028                         }
  2139                         }
  2029                         // We must resend EHLO after TLS negotiation
  2140                         //We must resend EHLO after TLS negotiation
  2030                         $this->smtp->hello($hello);
  2141                         $this->smtp->hello($hello);
  2031                     }
  2142                     }
  2032                     if ($this->SMTPAuth && !$this->smtp->authenticate(
  2143                     if (
  2033                         $this->Username,
  2144                         $this->SMTPAuth && !$this->smtp->authenticate(
  2034                         $this->Password,
  2145                             $this->Username,
  2035                         $this->AuthType,
  2146                             $this->Password,
  2036                         $this->oauth
  2147                             $this->AuthType,
  2037                     )) {
  2148                             $this->oauth
       
  2149                         )
       
  2150                     ) {
  2038                         throw new Exception($this->lang('authenticate'));
  2151                         throw new Exception($this->lang('authenticate'));
  2039                     }
  2152                     }
  2040 
  2153 
  2041                     return true;
  2154                     return true;
  2042                 } catch (Exception $exc) {
  2155                 } catch (Exception $exc) {
  2043                     $lastexception = $exc;
  2156                     $lastexception = $exc;
  2044                     $this->edebug($exc->getMessage());
  2157                     $this->edebug($exc->getMessage());
  2045                     // We must have connected, but then failed TLS or Auth, so close connection nicely
  2158                     //We must have connected, but then failed TLS or Auth, so close connection nicely
  2046                     $this->smtp->quit();
  2159                     $this->smtp->quit();
  2047                 }
  2160                 }
  2048             }
  2161             }
  2049         }
  2162         }
  2050         // If we get here, all connection attempts have failed, so close connection hard
  2163         //If we get here, all connection attempts have failed, so close connection hard
  2051         $this->smtp->close();
  2164         $this->smtp->close();
  2052         // As we've caught all exceptions, just report whatever the last one was
  2165         //As we've caught all exceptions, just report whatever the last one was
  2053         if ($this->exceptions && null !== $lastexception) {
  2166         if ($this->exceptions && null !== $lastexception) {
  2054             throw $lastexception;
  2167             throw $lastexception;
  2055         }
  2168         }
  2056 
  2169 
  2057         return false;
  2170         return false;
  2072      * Set the language for error messages.
  2185      * Set the language for error messages.
  2073      * Returns false if it cannot load the language file.
  2186      * Returns false if it cannot load the language file.
  2074      * The default language is English.
  2187      * The default language is English.
  2075      *
  2188      *
  2076      * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
  2189      * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
  2077      * @param string $lang_path Path to the language file directory, with trailing separator (slash)
  2190      * @param string $lang_path Path to the language file directory, with trailing separator (slash).D
       
  2191      *                          Do not set this from user input!
  2078      *
  2192      *
  2079      * @return bool
  2193      * @return bool
  2080      */
  2194      */
  2081     public function setLanguage($langcode = 'en', $lang_path = '')
  2195     public function setLanguage($langcode = 'en', $lang_path = '')
  2082     {
  2196     {
  2083         // Backwards compatibility for renamed language codes
  2197         //Backwards compatibility for renamed language codes
  2084         $renamed_langcodes = [
  2198         $renamed_langcodes = [
  2085             'br' => 'pt_br',
  2199             'br' => 'pt_br',
  2086             'cz' => 'cs',
  2200             'cz' => 'cs',
  2087             'dk' => 'da',
  2201             'dk' => 'da',
  2088             'no' => 'nb',
  2202             'no' => 'nb',
  2090             'rs' => 'sr',
  2204             'rs' => 'sr',
  2091             'tg' => 'tl',
  2205             'tg' => 'tl',
  2092             'am' => 'hy',
  2206             'am' => 'hy',
  2093         ];
  2207         ];
  2094 
  2208 
  2095         if (isset($renamed_langcodes[$langcode])) {
  2209         if (array_key_exists($langcode, $renamed_langcodes)) {
  2096             $langcode = $renamed_langcodes[$langcode];
  2210             $langcode = $renamed_langcodes[$langcode];
  2097         }
  2211         }
  2098 
  2212 
  2099         // Define full set of translatable strings in English
  2213         //Define full set of translatable strings in English
  2100         $PHPMAILER_LANG = [
  2214         $PHPMAILER_LANG = [
  2101             'authenticate' => 'SMTP Error: Could not authenticate.',
  2215             'authenticate' => 'SMTP Error: Could not authenticate.',
  2102             'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
  2216             'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
  2103             'data_not_accepted' => 'SMTP Error: data not accepted.',
  2217             'data_not_accepted' => 'SMTP Error: data not accepted.',
  2104             'empty_message' => 'Message body empty',
  2218             'empty_message' => 'Message body empty',
  2119             'smtp_error' => 'SMTP server error: ',
  2233             'smtp_error' => 'SMTP server error: ',
  2120             'variable_set' => 'Cannot set or reset variable: ',
  2234             'variable_set' => 'Cannot set or reset variable: ',
  2121             'extension_missing' => 'Extension missing: ',
  2235             'extension_missing' => 'Extension missing: ',
  2122         ];
  2236         ];
  2123         if (empty($lang_path)) {
  2237         if (empty($lang_path)) {
  2124             // Calculate an absolute path so it can work if CWD is not here
  2238             //Calculate an absolute path so it can work if CWD is not here
  2125             $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
  2239             $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
  2126         }
  2240         }
  2127         //Validate $langcode
  2241         //Validate $langcode
  2128         if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
  2242         if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
  2129             $langcode = 'en';
  2243             $langcode = 'en';
  2130         }
  2244         }
  2131         $foundlang = true;
  2245         $foundlang = true;
  2132         $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
  2246         $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
  2133         // There is no English translation file
  2247         //There is no English translation file
  2134         if ('en' !== $langcode) {
  2248         if ('en' !== $langcode) {
  2135             // Make sure language file path is readable
  2249             //Make sure language file path is readable
  2136             if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) {
  2250             if (!static::fileIsAccessible($lang_file)) {
  2137                 $foundlang = false;
  2251                 $foundlang = false;
  2138             } else {
  2252             } else {
  2139                 // Overwrite language-specific strings.
  2253                 //$foundlang = include $lang_file;
  2140                 // This way we'll never have missing translation keys.
  2254                 $lines = file($lang_file);
  2141                 $foundlang = include $lang_file;
  2255                 foreach ($lines as $line) {
       
  2256                     //Translation file lines look like this:
       
  2257                     //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
       
  2258                     //These files are parsed as text and not PHP so as to avoid the possibility of code injection
       
  2259                     //See https://blog.stevenlevithan.com/archives/match-quoted-string
       
  2260                     $matches = [];
       
  2261                     if (
       
  2262                         preg_match(
       
  2263                             '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/',
       
  2264                             $line,
       
  2265                             $matches
       
  2266                         ) &&
       
  2267                         //Ignore unknown translation keys
       
  2268                         array_key_exists($matches[1], $PHPMAILER_LANG)
       
  2269                     ) {
       
  2270                         //Overwrite language-specific strings so we'll never have missing translation keys.
       
  2271                         $PHPMAILER_LANG[$matches[1]] = (string)$matches[3];
       
  2272                     }
       
  2273                 }
  2142             }
  2274             }
  2143         }
  2275         }
  2144         $this->language = $PHPMAILER_LANG;
  2276         $this->language = $PHPMAILER_LANG;
  2145 
  2277 
  2146         return (bool) $foundlang; // Returns false if language not found
  2278         return $foundlang; //Returns false if language not found
  2147     }
  2279     }
  2148 
  2280 
  2149     /**
  2281     /**
  2150      * Get the array of strings for the current language.
  2282      * Get the array of strings for the current language.
  2151      *
  2283      *
  2185      *
  2317      *
  2186      * @return string
  2318      * @return string
  2187      */
  2319      */
  2188     public function addrFormat($addr)
  2320     public function addrFormat($addr)
  2189     {
  2321     {
  2190         if (empty($addr[1])) { // No name provided
  2322         if (empty($addr[1])) { //No name provided
  2191             return $this->secureHeader($addr[0]);
  2323             return $this->secureHeader($addr[0]);
  2192         }
  2324         }
  2193 
  2325 
  2194         return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
  2326         return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
  2195             ' <' . $this->secureHeader($addr[0]) . '>';
  2327             ' <' . $this->secureHeader($addr[0]) . '>';
  2212         if ($qp_mode) {
  2344         if ($qp_mode) {
  2213             $soft_break = sprintf(' =%s', static::$LE);
  2345             $soft_break = sprintf(' =%s', static::$LE);
  2214         } else {
  2346         } else {
  2215             $soft_break = static::$LE;
  2347             $soft_break = static::$LE;
  2216         }
  2348         }
  2217         // If utf-8 encoding is used, we will need to make sure we don't
  2349         //If utf-8 encoding is used, we will need to make sure we don't
  2218         // split multibyte characters when we wrap
  2350         //split multibyte characters when we wrap
  2219         $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
  2351         $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
  2220         $lelen = strlen(static::$LE);
  2352         $lelen = strlen(static::$LE);
  2221         $crlflen = strlen(static::$LE);
  2353         $crlflen = strlen(static::$LE);
  2222 
  2354 
  2223         $message = static::normalizeBreaks($message);
  2355         $message = static::normalizeBreaks($message);
  2313         $lookBack = 3;
  2445         $lookBack = 3;
  2314         while (!$foundSplitPos) {
  2446         while (!$foundSplitPos) {
  2315             $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
  2447             $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
  2316             $encodedCharPos = strpos($lastChunk, '=');
  2448             $encodedCharPos = strpos($lastChunk, '=');
  2317             if (false !== $encodedCharPos) {
  2449             if (false !== $encodedCharPos) {
  2318                 // Found start of encoded character byte within $lookBack block.
  2450                 //Found start of encoded character byte within $lookBack block.
  2319                 // Check the encoded byte value (the 2 chars after the '=')
  2451                 //Check the encoded byte value (the 2 chars after the '=')
  2320                 $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
  2452                 $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
  2321                 $dec = hexdec($hex);
  2453                 $dec = hexdec($hex);
  2322                 if ($dec < 128) {
  2454                 if ($dec < 128) {
  2323                     // Single byte character.
  2455                     //Single byte character.
  2324                     // If the encoded char was found at pos 0, it will fit
  2456                     //If the encoded char was found at pos 0, it will fit
  2325                     // otherwise reduce maxLength to start of the encoded char
  2457                     //otherwise reduce maxLength to start of the encoded char
  2326                     if ($encodedCharPos > 0) {
  2458                     if ($encodedCharPos > 0) {
  2327                         $maxLength -= $lookBack - $encodedCharPos;
  2459                         $maxLength -= $lookBack - $encodedCharPos;
  2328                     }
  2460                     }
  2329                     $foundSplitPos = true;
  2461                     $foundSplitPos = true;
  2330                 } elseif ($dec >= 192) {
  2462                 } elseif ($dec >= 192) {
  2331                     // First byte of a multi byte character
  2463                     //First byte of a multi byte character
  2332                     // Reduce maxLength to split at start of character
  2464                     //Reduce maxLength to split at start of character
  2333                     $maxLength -= $lookBack - $encodedCharPos;
  2465                     $maxLength -= $lookBack - $encodedCharPos;
  2334                     $foundSplitPos = true;
  2466                     $foundSplitPos = true;
  2335                 } elseif ($dec < 192) {
  2467                 } elseif ($dec < 192) {
  2336                     // Middle byte of a multi byte character, look further back
  2468                     //Middle byte of a multi byte character, look further back
  2337                     $lookBack += 3;
  2469                     $lookBack += 3;
  2338                 }
  2470                 }
  2339             } else {
  2471             } else {
  2340                 // No encoded character found
  2472                 //No encoded character found
  2341                 $foundSplitPos = true;
  2473                 $foundSplitPos = true;
  2342             }
  2474             }
  2343         }
  2475         }
  2344 
  2476 
  2345         return $maxLength;
  2477         return $maxLength;
  2379     {
  2511     {
  2380         $result = '';
  2512         $result = '';
  2381 
  2513 
  2382         $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
  2514         $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
  2383 
  2515 
  2384         // To be created automatically by mail()
  2516         //The To header is created automatically by mail(), so needs to be omitted here
  2385         if ($this->SingleTo) {
  2517         if ('mail' !== $this->Mailer) {
  2386             if ('mail' !== $this->Mailer) {
  2518             if ($this->SingleTo) {
  2387                 foreach ($this->to as $toaddr) {
  2519                 foreach ($this->to as $toaddr) {
  2388                     $this->SingleToArray[] = $this->addrFormat($toaddr);
  2520                     $this->SingleToArray[] = $this->addrFormat($toaddr);
  2389                 }
  2521                 }
  2390             }
  2522             } elseif (count($this->to) > 0) {
  2391         } elseif (count($this->to) > 0) {
       
  2392             if ('mail' !== $this->Mailer) {
       
  2393                 $result .= $this->addrAppend('To', $this->to);
  2523                 $result .= $this->addrAppend('To', $this->to);
  2394             }
  2524             } elseif (count($this->cc) === 0) {
  2395         } elseif (count($this->cc) === 0) {
  2525                 $result .= $this->headerLine('To', 'undisclosed-recipients:;');
  2396             $result .= $this->headerLine('To', 'undisclosed-recipients:;');
  2526             }
  2397         }
  2527         }
  2398 
       
  2399         $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
  2528         $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
  2400 
  2529 
  2401         // sendmail and mail() extract Cc from the header before sending
  2530         //sendmail and mail() extract Cc from the header before sending
  2402         if (count($this->cc) > 0) {
  2531         if (count($this->cc) > 0) {
  2403             $result .= $this->addrAppend('Cc', $this->cc);
  2532             $result .= $this->addrAppend('Cc', $this->cc);
  2404         }
  2533         }
  2405 
  2534 
  2406         // sendmail and mail() extract Bcc from the header before sending
  2535         //sendmail and mail() extract Bcc from the header before sending
  2407         if ((
  2536         if (
       
  2537             (
  2408                 'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
  2538                 'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
  2409             )
  2539             )
  2410             && count($this->bcc) > 0
  2540             && count($this->bcc) > 0
  2411         ) {
  2541         ) {
  2412             $result .= $this->addrAppend('Bcc', $this->bcc);
  2542             $result .= $this->addrAppend('Bcc', $this->bcc);
  2414 
  2544 
  2415         if (count($this->ReplyTo) > 0) {
  2545         if (count($this->ReplyTo) > 0) {
  2416             $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
  2546             $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
  2417         }
  2547         }
  2418 
  2548 
  2419         // mail() sets the subject itself
  2549         //mail() sets the subject itself
  2420         if ('mail' !== $this->Mailer) {
  2550         if ('mail' !== $this->Mailer) {
  2421             $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
  2551             $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
  2422         }
  2552         }
  2423 
  2553 
  2424         // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
  2554         //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
  2425         // https://tools.ietf.org/html/rfc5322#section-3.6.4
  2555         //https://tools.ietf.org/html/rfc5322#section-3.6.4
  2426         if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) {
  2556         if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) {
  2427             $this->lastMessageID = $this->MessageID;
  2557             $this->lastMessageID = $this->MessageID;
  2428         } else {
  2558         } else {
  2429             $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
  2559             $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
  2430         }
  2560         }
  2446 
  2576 
  2447         if ('' !== $this->ConfirmReadingTo) {
  2577         if ('' !== $this->ConfirmReadingTo) {
  2448             $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
  2578             $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
  2449         }
  2579         }
  2450 
  2580 
  2451         // Add custom headers
  2581         //Add custom headers
  2452         foreach ($this->CustomHeader as $header) {
  2582         foreach ($this->CustomHeader as $header) {
  2453             $result .= $this->headerLine(
  2583             $result .= $this->headerLine(
  2454                 trim($header[0]),
  2584                 trim($header[0]),
  2455                 $this->encodeHeader(trim($header[1]))
  2585                 $this->encodeHeader(trim($header[1]))
  2456             );
  2586             );
  2488             case 'alt_inline':
  2618             case 'alt_inline':
  2489                 $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
  2619                 $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
  2490                 $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
  2620                 $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
  2491                 break;
  2621                 break;
  2492             default:
  2622             default:
  2493                 // Catches case 'plain': and case '':
  2623                 //Catches case 'plain': and case '':
  2494                 $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
  2624                 $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
  2495                 $ismultipart = false;
  2625                 $ismultipart = false;
  2496                 break;
  2626                 break;
  2497         }
  2627         }
  2498         // RFC1341 part 5 says 7bit is assumed if not specified
  2628         //RFC1341 part 5 says 7bit is assumed if not specified
  2499         if (static::ENCODING_7BIT !== $this->Encoding) {
  2629         if (static::ENCODING_7BIT !== $this->Encoding) {
  2500             // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
  2630             //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
  2501             if ($ismultipart) {
  2631             if ($ismultipart) {
  2502                 if (static::ENCODING_8BIT === $this->Encoding) {
  2632                 if (static::ENCODING_8BIT === $this->Encoding) {
  2503                     $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
  2633                     $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
  2504                 }
  2634                 }
  2505                 // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
  2635                 //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
  2506             } else {
  2636             } else {
  2507                 $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
  2637                 $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
  2508             }
  2638             }
  2509         }
       
  2510 
       
  2511         if ('mail' !== $this->Mailer) {
       
  2512 //            $result .= static::$LE;
       
  2513         }
  2639         }
  2514 
  2640 
  2515         return $result;
  2641         return $result;
  2516     }
  2642     }
  2517 
  2643 
  2778                 $body .= $this->endBoundary($this->boundary[2]);
  2904                 $body .= $this->endBoundary($this->boundary[2]);
  2779                 $body .= static::$LE;
  2905                 $body .= static::$LE;
  2780                 $body .= $this->attachAll('attachment', $this->boundary[1]);
  2906                 $body .= $this->attachAll('attachment', $this->boundary[1]);
  2781                 break;
  2907                 break;
  2782             default:
  2908             default:
  2783                 // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
  2909                 //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
  2784                 //Reset the `Encoding` property in case we changed it for line length reasons
  2910                 //Reset the `Encoding` property in case we changed it for line length reasons
  2785                 $this->Encoding = $bodyEncoding;
  2911                 $this->Encoding = $bodyEncoding;
  2786                 $body .= $this->encodeString($this->Body, $this->Encoding);
  2912                 $body .= $this->encodeString($this->Body, $this->Encoding);
  2787                 break;
  2913                 break;
  2788         }
  2914         }
  2869             $encoding = $this->Encoding;
  2995             $encoding = $this->Encoding;
  2870         }
  2996         }
  2871         $result .= $this->textLine('--' . $boundary);
  2997         $result .= $this->textLine('--' . $boundary);
  2872         $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
  2998         $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
  2873         $result .= static::$LE;
  2999         $result .= static::$LE;
  2874         // RFC1341 part 5 says 7bit is assumed if not specified
  3000         //RFC1341 part 5 says 7bit is assumed if not specified
  2875         if (static::ENCODING_7BIT !== $encoding) {
  3001         if (static::ENCODING_7BIT !== $encoding) {
  2876             $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
  3002             $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
  2877         }
  3003         }
  2878         $result .= static::$LE;
  3004         $result .= static::$LE;
  2879 
  3005 
  2948      * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
  3074      * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
  2949      *
  3075      *
  2950      * @param string $path        Path to the attachment
  3076      * @param string $path        Path to the attachment
  2951      * @param string $name        Overrides the attachment name
  3077      * @param string $name        Overrides the attachment name
  2952      * @param string $encoding    File encoding (see $Encoding)
  3078      * @param string $encoding    File encoding (see $Encoding)
  2953      * @param string $type        File extension (MIME) type
  3079      * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
  2954      * @param string $disposition Disposition to use
  3080      * @param string $disposition Disposition to use
  2955      *
  3081      *
  2956      * @throws Exception
  3082      * @throws Exception
  2957      *
  3083      *
  2958      * @return bool
  3084      * @return bool
  2963         $encoding = self::ENCODING_BASE64,
  3089         $encoding = self::ENCODING_BASE64,
  2964         $type = '',
  3090         $type = '',
  2965         $disposition = 'attachment'
  3091         $disposition = 'attachment'
  2966     ) {
  3092     ) {
  2967         try {
  3093         try {
  2968             if (!static::isPermittedPath($path) || !@is_file($path) || !is_readable($path)) {
  3094             if (!static::fileIsAccessible($path)) {
  2969                 throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
  3095                 throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
  2970             }
  3096             }
  2971 
  3097 
  2972             // If a MIME type is not specified, try to work it out from the file name
  3098             //If a MIME type is not specified, try to work it out from the file name
  2973             if ('' === $type) {
  3099             if ('' === $type) {
  2974                 $type = static::filenameToType($path);
  3100                 $type = static::filenameToType($path);
  2975             }
  3101             }
  2976 
  3102 
  2977             $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
  3103             $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
  2978             if ('' === $name) {
  3104             if ('' === $name) {
  2979                 $name = $filename;
  3105                 $name = $filename;
  2980             }
  3106             }
  2981 
       
  2982             if (!$this->validateEncoding($encoding)) {
  3107             if (!$this->validateEncoding($encoding)) {
  2983                 throw new Exception($this->lang('encoding') . $encoding);
  3108                 throw new Exception($this->lang('encoding') . $encoding);
  2984             }
  3109             }
  2985 
  3110 
  2986             $this->attachment[] = [
  3111             $this->attachment[] = [
  2987                 0 => $path,
  3112                 0 => $path,
  2988                 1 => $filename,
  3113                 1 => $filename,
  2989                 2 => $name,
  3114                 2 => $name,
  2990                 3 => $encoding,
  3115                 3 => $encoding,
  2991                 4 => $type,
  3116                 4 => $type,
  2992                 5 => false, // isStringAttachment
  3117                 5 => false, //isStringAttachment
  2993                 6 => $disposition,
  3118                 6 => $disposition,
  2994                 7 => $name,
  3119                 7 => $name,
  2995             ];
  3120             ];
  2996         } catch (Exception $exc) {
  3121         } catch (Exception $exc) {
  2997             $this->setError($exc->getMessage());
  3122             $this->setError($exc->getMessage());
  3027      *
  3152      *
  3028      * @return string
  3153      * @return string
  3029      */
  3154      */
  3030     protected function attachAll($disposition_type, $boundary)
  3155     protected function attachAll($disposition_type, $boundary)
  3031     {
  3156     {
  3032         // Return text of body
  3157         //Return text of body
  3033         $mime = [];
  3158         $mime = [];
  3034         $cidUniq = [];
  3159         $cidUniq = [];
  3035         $incl = [];
  3160         $incl = [];
  3036 
  3161 
  3037         // Add all attachments
  3162         //Add all attachments
  3038         foreach ($this->attachment as $attachment) {
  3163         foreach ($this->attachment as $attachment) {
  3039             // Check if it is a valid disposition_filter
  3164             //Check if it is a valid disposition_filter
  3040             if ($attachment[6] === $disposition_type) {
  3165             if ($attachment[6] === $disposition_type) {
  3041                 // Check for string attachment
  3166                 //Check for string attachment
  3042                 $string = '';
  3167                 $string = '';
  3043                 $path = '';
  3168                 $path = '';
  3044                 $bString = $attachment[5];
  3169                 $bString = $attachment[5];
  3045                 if ($bString) {
  3170                 if ($bString) {
  3046                     $string = $attachment[0];
  3171                     $string = $attachment[0];
  3077                         'Content-Type: %s%s',
  3202                         'Content-Type: %s%s',
  3078                         $type,
  3203                         $type,
  3079                         static::$LE
  3204                         static::$LE
  3080                     );
  3205                     );
  3081                 }
  3206                 }
  3082                 // RFC1341 part 5 says 7bit is assumed if not specified
  3207                 //RFC1341 part 5 says 7bit is assumed if not specified
  3083                 if (static::ENCODING_7BIT !== $encoding) {
  3208                 if (static::ENCODING_7BIT !== $encoding) {
  3084                     $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
  3209                     $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
  3085                 }
  3210                 }
  3086 
  3211 
  3087                 //Only set Content-IDs on inline attachments
  3212                 //Only set Content-IDs on inline attachments
  3088                 if ((string) $cid !== '' && $disposition === 'inline') {
  3213                 if ((string) $cid !== '' && $disposition === 'inline') {
  3089                     $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
  3214                     $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
  3090                 }
  3215                 }
  3091 
  3216 
  3092                 // Allow for bypassing the Content-Disposition header
  3217                 //Allow for bypassing the Content-Disposition header
  3093                 if (!empty($disposition)) {
  3218                 if (!empty($disposition)) {
  3094                     $encoded_name = $this->encodeHeader($this->secureHeader($name));
  3219                     $encoded_name = $this->encodeHeader($this->secureHeader($name));
  3095                     if (!empty($encoded_name)) {
  3220                     if (!empty($encoded_name)) {
  3096                         $mime[] = sprintf(
  3221                         $mime[] = sprintf(
  3097                             'Content-Disposition: %s; filename=%s%s',
  3222                             'Content-Disposition: %s; filename=%s%s',
  3108                     }
  3233                     }
  3109                 } else {
  3234                 } else {
  3110                     $mime[] = static::$LE;
  3235                     $mime[] = static::$LE;
  3111                 }
  3236                 }
  3112 
  3237 
  3113                 // Encode as string attachment
  3238                 //Encode as string attachment
  3114                 if ($bString) {
  3239                 if ($bString) {
  3115                     $mime[] = $this->encodeString($string, $encoding);
  3240                     $mime[] = $this->encodeString($string, $encoding);
  3116                 } else {
  3241                 } else {
  3117                     $mime[] = $this->encodeFile($path, $encoding);
  3242                     $mime[] = $this->encodeFile($path, $encoding);
  3118                 }
  3243                 }
  3138      * @return string
  3263      * @return string
  3139      */
  3264      */
  3140     protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
  3265     protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
  3141     {
  3266     {
  3142         try {
  3267         try {
  3143             if (!static::isPermittedPath($path) || !file_exists($path) || !is_readable($path)) {
  3268             if (!static::fileIsAccessible($path)) {
  3144                 throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
  3269                 throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
  3145             }
  3270             }
  3146             $file_buffer = file_get_contents($path);
  3271             $file_buffer = file_get_contents($path);
  3147             if (false === $file_buffer) {
  3272             if (false === $file_buffer) {
  3148                 throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
  3273                 throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
  3184                 );
  3309                 );
  3185                 break;
  3310                 break;
  3186             case static::ENCODING_7BIT:
  3311             case static::ENCODING_7BIT:
  3187             case static::ENCODING_8BIT:
  3312             case static::ENCODING_8BIT:
  3188                 $encoded = static::normalizeBreaks($str);
  3313                 $encoded = static::normalizeBreaks($str);
  3189                 // Make sure it ends with a line break
  3314                 //Make sure it ends with a line break
  3190                 if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
  3315                 if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
  3191                     $encoded .= static::$LE;
  3316                     $encoded .= static::$LE;
  3192                 }
  3317                 }
  3193                 break;
  3318                 break;
  3194             case static::ENCODING_BINARY:
  3319             case static::ENCODING_BINARY:
  3222     {
  3347     {
  3223         $matchcount = 0;
  3348         $matchcount = 0;
  3224         switch (strtolower($position)) {
  3349         switch (strtolower($position)) {
  3225             case 'phrase':
  3350             case 'phrase':
  3226                 if (!preg_match('/[\200-\377]/', $str)) {
  3351                 if (!preg_match('/[\200-\377]/', $str)) {
  3227                     // Can't use addslashes as we don't know the value of magic_quotes_sybase
  3352                     //Can't use addslashes as we don't know the value of magic_quotes_sybase
  3228                     $encoded = addcslashes($str, "\0..\37\177\\\"");
  3353                     $encoded = addcslashes($str, "\0..\37\177\\\"");
  3229                     if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
  3354                     if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
  3230                         return $encoded;
  3355                         return $encoded;
  3231                     }
  3356                     }
  3232 
  3357 
  3248             $charset = $this->CharSet;
  3373             $charset = $this->CharSet;
  3249         } else {
  3374         } else {
  3250             $charset = static::CHARSET_ASCII;
  3375             $charset = static::CHARSET_ASCII;
  3251         }
  3376         }
  3252 
  3377 
  3253         // Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
  3378         //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
  3254         $overhead = 8 + strlen($charset);
  3379         $overhead = 8 + strlen($charset);
  3255 
  3380 
  3256         if ('mail' === $this->Mailer) {
  3381         if ('mail' === $this->Mailer) {
  3257             $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
  3382             $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
  3258         } else {
  3383         } else {
  3259             $maxlen = static::MAX_LINE_LENGTH - $overhead;
  3384             $maxlen = static::MAX_LINE_LENGTH - $overhead;
  3260         }
  3385         }
  3261 
  3386 
  3262         // Select the encoding that produces the shortest output and/or prevents corruption.
  3387         //Select the encoding that produces the shortest output and/or prevents corruption.
  3263         if ($matchcount > strlen($str) / 3) {
  3388         if ($matchcount > strlen($str) / 3) {
  3264             // More than 1/3 of the content needs encoding, use B-encode.
  3389             //More than 1/3 of the content needs encoding, use B-encode.
  3265             $encoding = 'B';
  3390             $encoding = 'B';
  3266         } elseif ($matchcount > 0) {
  3391         } elseif ($matchcount > 0) {
  3267             // Less than 1/3 of the content needs encoding, use Q-encode.
  3392             //Less than 1/3 of the content needs encoding, use Q-encode.
  3268             $encoding = 'Q';
  3393             $encoding = 'Q';
  3269         } elseif (strlen($str) > $maxlen) {
  3394         } elseif (strlen($str) > $maxlen) {
  3270             // No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
  3395             //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
  3271             $encoding = 'Q';
  3396             $encoding = 'Q';
  3272         } else {
  3397         } else {
  3273             // No reformatting needed
  3398             //No reformatting needed
  3274             $encoding = false;
  3399             $encoding = false;
  3275         }
  3400         }
  3276 
  3401 
  3277         switch ($encoding) {
  3402         switch ($encoding) {
  3278             case 'B':
  3403             case 'B':
  3279                 if ($this->hasMultiBytes($str)) {
  3404                 if ($this->hasMultiBytes($str)) {
  3280                     // Use a custom function which correctly encodes and wraps long
  3405                     //Use a custom function which correctly encodes and wraps long
  3281                     // multibyte strings without breaking lines within a character
  3406                     //multibyte strings without breaking lines within a character
  3282                     $encoded = $this->base64EncodeWrapMB($str, "\n");
  3407                     $encoded = $this->base64EncodeWrapMB($str, "\n");
  3283                 } else {
  3408                 } else {
  3284                     $encoded = base64_encode($str);
  3409                     $encoded = base64_encode($str);
  3285                     $maxlen -= $maxlen % 4;
  3410                     $maxlen -= $maxlen % 4;
  3286                     $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
  3411                     $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
  3311     {
  3436     {
  3312         if (function_exists('mb_strlen')) {
  3437         if (function_exists('mb_strlen')) {
  3313             return strlen($str) > mb_strlen($str, $this->CharSet);
  3438             return strlen($str) > mb_strlen($str, $this->CharSet);
  3314         }
  3439         }
  3315 
  3440 
  3316         // Assume no multibytes (we can't handle without mbstring functions anyway)
  3441         //Assume no multibytes (we can't handle without mbstring functions anyway)
  3317         return false;
  3442         return false;
  3318     }
  3443     }
  3319 
  3444 
  3320     /**
  3445     /**
  3321      * Does a string contain any 8-bit chars (in any charset)?
  3446      * Does a string contain any 8-bit chars (in any charset)?
  3349         if (null === $linebreak) {
  3474         if (null === $linebreak) {
  3350             $linebreak = static::$LE;
  3475             $linebreak = static::$LE;
  3351         }
  3476         }
  3352 
  3477 
  3353         $mb_length = mb_strlen($str, $this->CharSet);
  3478         $mb_length = mb_strlen($str, $this->CharSet);
  3354         // Each line must have length <= 75, including $start and $end
  3479         //Each line must have length <= 75, including $start and $end
  3355         $length = 75 - strlen($start) - strlen($end);
  3480         $length = 75 - strlen($start) - strlen($end);
  3356         // Average multi-byte ratio
  3481         //Average multi-byte ratio
  3357         $ratio = $mb_length / strlen($str);
  3482         $ratio = $mb_length / strlen($str);
  3358         // Base64 has a 4:3 ratio
  3483         //Base64 has a 4:3 ratio
  3359         $avgLength = floor($length * $ratio * .75);
  3484         $avgLength = floor($length * $ratio * .75);
  3360 
  3485 
  3361         $offset = 0;
  3486         $offset = 0;
  3362         for ($i = 0; $i < $mb_length; $i += $offset) {
  3487         for ($i = 0; $i < $mb_length; $i += $offset) {
  3363             $lookBack = 0;
  3488             $lookBack = 0;
  3368                 ++$lookBack;
  3493                 ++$lookBack;
  3369             } while (strlen($chunk) > $length);
  3494             } while (strlen($chunk) > $length);
  3370             $encoded .= $chunk . $linebreak;
  3495             $encoded .= $chunk . $linebreak;
  3371         }
  3496         }
  3372 
  3497 
  3373         // Chomp the last linefeed
  3498         //Chomp the last linefeed
  3374         return substr($encoded, 0, -strlen($linebreak));
  3499         return substr($encoded, 0, -strlen($linebreak));
  3375     }
  3500     }
  3376 
  3501 
  3377     /**
  3502     /**
  3378      * Encode a string in quoted-printable format.
  3503      * Encode a string in quoted-printable format.
  3397      *
  3522      *
  3398      * @return string
  3523      * @return string
  3399      */
  3524      */
  3400     public function encodeQ($str, $position = 'text')
  3525     public function encodeQ($str, $position = 'text')
  3401     {
  3526     {
  3402         // There should not be any EOL in the string
  3527         //There should not be any EOL in the string
  3403         $pattern = '';
  3528         $pattern = '';
  3404         $encoded = str_replace(["\r", "\n"], '', $str);
  3529         $encoded = str_replace(["\r", "\n"], '', $str);
  3405         switch (strtolower($position)) {
  3530         switch (strtolower($position)) {
  3406             case 'phrase':
  3531             case 'phrase':
  3407                 // RFC 2047 section 5.3
  3532                 //RFC 2047 section 5.3
  3408                 $pattern = '^A-Za-z0-9!*+\/ -';
  3533                 $pattern = '^A-Za-z0-9!*+\/ -';
  3409                 break;
  3534                 break;
  3410             /*
  3535             /*
  3411              * RFC 2047 section 5.2.
  3536              * RFC 2047 section 5.2.
  3412              * Build $pattern without including delimiters and []
  3537              * Build $pattern without including delimiters and []
  3415             case 'comment':
  3540             case 'comment':
  3416                 $pattern = '\(\)"';
  3541                 $pattern = '\(\)"';
  3417             /* Intentional fall through */
  3542             /* Intentional fall through */
  3418             case 'text':
  3543             case 'text':
  3419             default:
  3544             default:
  3420                 // RFC 2047 section 5.1
  3545                 //RFC 2047 section 5.1
  3421                 // Replace every high ascii, control, =, ? and _ characters
  3546                 //Replace every high ascii, control, =, ? and _ characters
  3422                 $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
  3547                 $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
  3423                 break;
  3548                 break;
  3424         }
  3549         }
  3425         $matches = [];
  3550         $matches = [];
  3426         if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
  3551         if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
  3427             // If the string contains an '=', make sure it's the first thing we replace
  3552             //If the string contains an '=', make sure it's the first thing we replace
  3428             // so as to avoid double-encoding
  3553             //so as to avoid double-encoding
  3429             $eqkey = array_search('=', $matches[0], true);
  3554             $eqkey = array_search('=', $matches[0], true);
  3430             if (false !== $eqkey) {
  3555             if (false !== $eqkey) {
  3431                 unset($matches[0][$eqkey]);
  3556                 unset($matches[0][$eqkey]);
  3432                 array_unshift($matches[0], '=');
  3557                 array_unshift($matches[0], '=');
  3433             }
  3558             }
  3434             foreach (array_unique($matches[0]) as $char) {
  3559             foreach (array_unique($matches[0]) as $char) {
  3435                 $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
  3560                 $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
  3436             }
  3561             }
  3437         }
  3562         }
  3438         // Replace spaces with _ (more readable than =20)
  3563         //Replace spaces with _ (more readable than =20)
  3439         // RFC 2047 section 4.2(2)
  3564         //RFC 2047 section 4.2(2)
  3440         return str_replace(' ', '_', $encoded);
  3565         return str_replace(' ', '_', $encoded);
  3441     }
  3566     }
  3442 
  3567 
  3443     /**
  3568     /**
  3444      * Add a string or binary attachment (non-filesystem).
  3569      * Add a string or binary attachment (non-filesystem).
  3461         $encoding = self::ENCODING_BASE64,
  3586         $encoding = self::ENCODING_BASE64,
  3462         $type = '',
  3587         $type = '',
  3463         $disposition = 'attachment'
  3588         $disposition = 'attachment'
  3464     ) {
  3589     ) {
  3465         try {
  3590         try {
  3466             // If a MIME type is not specified, try to work it out from the file name
  3591             //If a MIME type is not specified, try to work it out from the file name
  3467             if ('' === $type) {
  3592             if ('' === $type) {
  3468                 $type = static::filenameToType($filename);
  3593                 $type = static::filenameToType($filename);
  3469             }
  3594             }
  3470 
  3595 
  3471             if (!$this->validateEncoding($encoding)) {
  3596             if (!$this->validateEncoding($encoding)) {
  3472                 throw new Exception($this->lang('encoding') . $encoding);
  3597                 throw new Exception($this->lang('encoding') . $encoding);
  3473             }
  3598             }
  3474 
  3599 
  3475             // Append to $attachment array
  3600             //Append to $attachment array
  3476             $this->attachment[] = [
  3601             $this->attachment[] = [
  3477                 0 => $string,
  3602                 0 => $string,
  3478                 1 => $filename,
  3603                 1 => $filename,
  3479                 2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
  3604                 2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
  3480                 3 => $encoding,
  3605                 3 => $encoding,
  3481                 4 => $type,
  3606                 4 => $type,
  3482                 5 => true, // isStringAttachment
  3607                 5 => true, //isStringAttachment
  3483                 6 => $disposition,
  3608                 6 => $disposition,
  3484                 7 => 0,
  3609                 7 => 0,
  3485             ];
  3610             ];
  3486         } catch (Exception $exc) {
  3611         } catch (Exception $exc) {
  3487             $this->setError($exc->getMessage());
  3612             $this->setError($exc->getMessage());
  3524         $encoding = self::ENCODING_BASE64,
  3649         $encoding = self::ENCODING_BASE64,
  3525         $type = '',
  3650         $type = '',
  3526         $disposition = 'inline'
  3651         $disposition = 'inline'
  3527     ) {
  3652     ) {
  3528         try {
  3653         try {
  3529             if (!static::isPermittedPath($path) || !@is_file($path) || !is_readable($path)) {
  3654             if (!static::fileIsAccessible($path)) {
  3530                 throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
  3655                 throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
  3531             }
  3656             }
  3532 
  3657 
  3533             // If a MIME type is not specified, try to work it out from the file name
  3658             //If a MIME type is not specified, try to work it out from the file name
  3534             if ('' === $type) {
  3659             if ('' === $type) {
  3535                 $type = static::filenameToType($path);
  3660                 $type = static::filenameToType($path);
  3536             }
  3661             }
  3537 
  3662 
  3538             if (!$this->validateEncoding($encoding)) {
  3663             if (!$this->validateEncoding($encoding)) {
  3542             $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
  3667             $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
  3543             if ('' === $name) {
  3668             if ('' === $name) {
  3544                 $name = $filename;
  3669                 $name = $filename;
  3545             }
  3670             }
  3546 
  3671 
  3547             // Append to $attachment array
  3672             //Append to $attachment array
  3548             $this->attachment[] = [
  3673             $this->attachment[] = [
  3549                 0 => $path,
  3674                 0 => $path,
  3550                 1 => $filename,
  3675                 1 => $filename,
  3551                 2 => $name,
  3676                 2 => $name,
  3552                 3 => $encoding,
  3677                 3 => $encoding,
  3553                 4 => $type,
  3678                 4 => $type,
  3554                 5 => false, // isStringAttachment
  3679                 5 => false, //isStringAttachment
  3555                 6 => $disposition,
  3680                 6 => $disposition,
  3556                 7 => $cid,
  3681                 7 => $cid,
  3557             ];
  3682             ];
  3558         } catch (Exception $exc) {
  3683         } catch (Exception $exc) {
  3559             $this->setError($exc->getMessage());
  3684             $this->setError($exc->getMessage());
  3594         $encoding = self::ENCODING_BASE64,
  3719         $encoding = self::ENCODING_BASE64,
  3595         $type = '',
  3720         $type = '',
  3596         $disposition = 'inline'
  3721         $disposition = 'inline'
  3597     ) {
  3722     ) {
  3598         try {
  3723         try {
  3599             // If a MIME type is not specified, try to work it out from the name
  3724             //If a MIME type is not specified, try to work it out from the name
  3600             if ('' === $type && !empty($name)) {
  3725             if ('' === $type && !empty($name)) {
  3601                 $type = static::filenameToType($name);
  3726                 $type = static::filenameToType($name);
  3602             }
  3727             }
  3603 
  3728 
  3604             if (!$this->validateEncoding($encoding)) {
  3729             if (!$this->validateEncoding($encoding)) {
  3605                 throw new Exception($this->lang('encoding') . $encoding);
  3730                 throw new Exception($this->lang('encoding') . $encoding);
  3606             }
  3731             }
  3607 
  3732 
  3608             // Append to $attachment array
  3733             //Append to $attachment array
  3609             $this->attachment[] = [
  3734             $this->attachment[] = [
  3610                 0 => $string,
  3735                 0 => $string,
  3611                 1 => $name,
  3736                 1 => $name,
  3612                 2 => $name,
  3737                 2 => $name,
  3613                 3 => $encoding,
  3738                 3 => $encoding,
  3614                 4 => $type,
  3739                 4 => $type,
  3615                 5 => true, // isStringAttachment
  3740                 5 => true, //isStringAttachment
  3616                 6 => $disposition,
  3741                 6 => $disposition,
  3617                 7 => $cid,
  3742                 7 => $cid,
  3618             ];
  3743             ];
  3619         } catch (Exception $exc) {
  3744         } catch (Exception $exc) {
  3620             $this->setError($exc->getMessage());
  3745             $this->setError($exc->getMessage());
  3830      *
  3955      *
  3831      * @return string
  3956      * @return string
  3832      */
  3957      */
  3833     public static function rfcDate()
  3958     public static function rfcDate()
  3834     {
  3959     {
  3835         // Set the time zone to whatever the default is to avoid 500 errors
  3960         //Set the time zone to whatever the default is to avoid 500 errors
  3836         // Will default to UTC if it's not set properly in php.ini
  3961         //Will default to UTC if it's not set properly in php.ini
  3837         date_default_timezone_set(@date_default_timezone_get());
  3962         date_default_timezone_set(@date_default_timezone_get());
  3838 
  3963 
  3839         return date('D, j M Y H:i:s O');
  3964         return date('D, j M Y H:i:s O');
  3840     }
  3965     }
  3841 
  3966 
  3873      * @return bool
  3998      * @return bool
  3874      */
  3999      */
  3875     public static function isValidHost($host)
  4000     public static function isValidHost($host)
  3876     {
  4001     {
  3877         //Simple syntax limits
  4002         //Simple syntax limits
  3878         if (empty($host)
  4003         if (
       
  4004             empty($host)
  3879             || !is_string($host)
  4005             || !is_string($host)
  3880             || strlen($host) > 256
  4006             || strlen($host) > 256
  3881             || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $host)
  4007             || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $host)
  3882         ) {
  4008         ) {
  3883             return false;
  4009             return false;
  3908      * @return string
  4034      * @return string
  3909      */
  4035      */
  3910     protected function lang($key)
  4036     protected function lang($key)
  3911     {
  4037     {
  3912         if (count($this->language) < 1) {
  4038         if (count($this->language) < 1) {
  3913             $this->setLanguage(); // set the default language
  4039             $this->setLanguage(); //Set the default language
  3914         }
  4040         }
  3915 
  4041 
  3916         if (array_key_exists($key, $this->language)) {
  4042         if (array_key_exists($key, $this->language)) {
  3917             if ('smtp_connect_failed' === $key) {
  4043             if ('smtp_connect_failed' === $key) {
  3918                 //Include a link to troubleshooting docs on SMTP connection failure
  4044                 //Include a link to troubleshooting docs on SMTP connection failure.
  3919                 //this is by far the biggest cause of support questions
  4045                 //This is by far the biggest cause of support questions
  3920                 //but it's usually not PHPMailer's fault.
  4046                 //but it's usually not PHPMailer's fault.
  3921                 return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
  4047                 return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
  3922             }
  4048             }
  3923 
  4049 
  3924             return $this->language[$key];
  4050             return $this->language[$key];
  3949      * @throws Exception
  4075      * @throws Exception
  3950      */
  4076      */
  3951     public function addCustomHeader($name, $value = null)
  4077     public function addCustomHeader($name, $value = null)
  3952     {
  4078     {
  3953         if (null === $value && strpos($name, ':') !== false) {
  4079         if (null === $value && strpos($name, ':') !== false) {
  3954             // Value passed in as name:value
  4080             //Value passed in as name:value
  3955             list($name, $value) = explode(':', $name, 2);
  4081             list($name, $value) = explode(':', $name, 2);
  3956         }
  4082         }
  3957         $name = trim($name);
  4083         $name = trim($name);
  3958         $value = trim($value);
  4084         $value = trim($value);
  3959         //Ensure name is not empty, and that neither name nor value contain line breaks
  4085         //Ensure name is not empty, and that neither name nor value contain line breaks
  3991      * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
  4117      * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
  3992      *
  4118      *
  3993      * @param string        $message  HTML message string
  4119      * @param string        $message  HTML message string
  3994      * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
  4120      * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
  3995      * @param bool|callable $advanced Whether to use the internal HTML to text converter
  4121      * @param bool|callable $advanced Whether to use the internal HTML to text converter
  3996      *                                or your own custom converter @return string $message The transformed message Body
  4122      *                                or your own custom converter
       
  4123      * @return string The transformed message body
  3997      *
  4124      *
  3998      * @throws Exception
  4125      * @throws Exception
  3999      *
  4126      *
  4000      * @see PHPMailer::html2text()
  4127      * @see PHPMailer::html2text()
  4001      */
  4128      */
  4002     public function msgHTML($message, $basedir = '', $advanced = false)
  4129     public function msgHTML($message, $basedir = '', $advanced = false)
  4003     {
  4130     {
  4004         preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
  4131         preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
  4005         if (array_key_exists(2, $images)) {
  4132         if (array_key_exists(2, $images)) {
  4006             if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
  4133             if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
  4007                 // Ensure $basedir has a trailing /
  4134                 //Ensure $basedir has a trailing /
  4008                 $basedir .= '/';
  4135                 $basedir .= '/';
  4009             }
  4136             }
  4010             foreach ($images[2] as $imgindex => $url) {
  4137             foreach ($images[2] as $imgindex => $url) {
  4011                 // Convert data URIs into embedded images
  4138                 //Convert data URIs into embedded images
  4012                 //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
  4139                 //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
  4013                 $match = [];
  4140                 $match = [];
  4014                 if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
  4141                 if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
  4015                     if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
  4142                     if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
  4016                         $data = base64_decode($match[3]);
  4143                         $data = base64_decode($match[3]);
  4020                         //Not recognised so leave it alone
  4147                         //Not recognised so leave it alone
  4021                         continue;
  4148                         continue;
  4022                     }
  4149                     }
  4023                     //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
  4150                     //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
  4024                     //will only be embedded once, even if it used a different encoding
  4151                     //will only be embedded once, even if it used a different encoding
  4025                     $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; // RFC2392 S 2
  4152                     $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2
  4026 
  4153 
  4027                     if (!$this->cidExists($cid)) {
  4154                     if (!$this->cidExists($cid)) {
  4028                         $this->addStringEmbeddedImage(
  4155                         $this->addStringEmbeddedImage(
  4029                             $data,
  4156                             $data,
  4030                             $cid,
  4157                             $cid,
  4038                         $images[1][$imgindex] . '="cid:' . $cid . '"',
  4165                         $images[1][$imgindex] . '="cid:' . $cid . '"',
  4039                         $message
  4166                         $message
  4040                     );
  4167                     );
  4041                     continue;
  4168                     continue;
  4042                 }
  4169                 }
  4043                 if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
  4170                 if (
       
  4171                     //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
  4044                     !empty($basedir)
  4172                     !empty($basedir)
  4045                     // Ignore URLs containing parent dir traversal (..)
  4173                     //Ignore URLs containing parent dir traversal (..)
  4046                     && (strpos($url, '..') === false)
  4174                     && (strpos($url, '..') === false)
  4047                     // Do not change urls that are already inline images
  4175                     //Do not change urls that are already inline images
  4048                     && 0 !== strpos($url, 'cid:')
  4176                     && 0 !== strpos($url, 'cid:')
  4049                     // Do not change absolute URLs, including anonymous protocol
  4177                     //Do not change absolute URLs, including anonymous protocol
  4050                     && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
  4178                     && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
  4051                 ) {
  4179                 ) {
  4052                     $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
  4180                     $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
  4053                     $directory = dirname($url);
  4181                     $directory = dirname($url);
  4054                     if ('.' === $directory) {
  4182                     if ('.' === $directory) {
  4055                         $directory = '';
  4183                         $directory = '';
  4056                     }
  4184                     }
  4057                     // RFC2392 S 2
  4185                     //RFC2392 S 2
  4058                     $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
  4186                     $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
  4059                     if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
  4187                     if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
  4060                         $basedir .= '/';
  4188                         $basedir .= '/';
  4061                     }
  4189                     }
  4062                     if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
  4190                     if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
  4063                         $directory .= '/';
  4191                         $directory .= '/';
  4064                     }
  4192                     }
  4065                     if ($this->addEmbeddedImage(
  4193                     if (
  4066                         $basedir . $directory . $filename,
  4194                         $this->addEmbeddedImage(
  4067                         $cid,
  4195                             $basedir . $directory . $filename,
  4068                         $filename,
  4196                             $cid,
  4069                         static::ENCODING_BASE64,
  4197                             $filename,
  4070                         static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
  4198                             static::ENCODING_BASE64,
  4071                     )
  4199                             static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
       
  4200                         )
  4072                     ) {
  4201                     ) {
  4073                         $message = preg_replace(
  4202                         $message = preg_replace(
  4074                             '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
  4203                             '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
  4075                             $images[1][$imgindex] . '="cid:' . $cid . '"',
  4204                             $images[1][$imgindex] . '="cid:' . $cid . '"',
  4076                             $message
  4205                             $message
  4078                     }
  4207                     }
  4079                 }
  4208                 }
  4080             }
  4209             }
  4081         }
  4210         }
  4082         $this->isHTML();
  4211         $this->isHTML();
  4083         // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
  4212         //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
  4084         $this->Body = static::normalizeBreaks($message);
  4213         $this->Body = static::normalizeBreaks($message);
  4085         $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
  4214         $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
  4086         if (!$this->alternativeExists()) {
  4215         if (!$this->alternativeExists()) {
  4087             $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
  4216             $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
  4088                 . static::$LE;
  4217                 . static::$LE;
  4097      * Note - older versions of this function used a bundled advanced converter
  4226      * Note - older versions of this function used a bundled advanced converter
  4098      * which was removed for license reasons in #232.
  4227      * which was removed for license reasons in #232.
  4099      * Example usage:
  4228      * Example usage:
  4100      *
  4229      *
  4101      * ```php
  4230      * ```php
  4102      * // Use default conversion
  4231      * //Use default conversion
  4103      * $plain = $mail->html2text($html);
  4232      * $plain = $mail->html2text($html);
  4104      * // Use your own custom converter
  4233      * //Use your own custom converter
  4105      * $plain = $mail->html2text($html, function($html) {
  4234      * $plain = $mail->html2text($html, function($html) {
  4106      *     $converter = new MyHtml2text($html);
  4235      *     $converter = new MyHtml2text($html);
  4107      *     return $converter->get_text();
  4236      *     return $converter->get_text();
  4108      * });
  4237      * });
  4109      * ```
  4238      * ```
  4115      * @return string
  4244      * @return string
  4116      */
  4245      */
  4117     public function html2text($html, $advanced = false)
  4246     public function html2text($html, $advanced = false)
  4118     {
  4247     {
  4119         if (is_callable($advanced)) {
  4248         if (is_callable($advanced)) {
  4120             return $advanced($html);
  4249             return call_user_func($advanced, $html);
  4121         }
  4250         }
  4122 
  4251 
  4123         return html_entity_decode(
  4252         return html_entity_decode(
  4124             trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
  4253             trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
  4125             ENT_QUOTES,
  4254             ENT_QUOTES,
  4214             'jpg' => 'image/jpeg',
  4343             'jpg' => 'image/jpeg',
  4215             'png' => 'image/png',
  4344             'png' => 'image/png',
  4216             'tiff' => 'image/tiff',
  4345             'tiff' => 'image/tiff',
  4217             'tif' => 'image/tiff',
  4346             'tif' => 'image/tiff',
  4218             'webp' => 'image/webp',
  4347             'webp' => 'image/webp',
       
  4348             'avif' => 'image/avif',
  4219             'heif' => 'image/heif',
  4349             'heif' => 'image/heif',
  4220             'heifs' => 'image/heif-sequence',
  4350             'heifs' => 'image/heif-sequence',
  4221             'heic' => 'image/heic',
  4351             'heic' => 'image/heic',
  4222             'heics' => 'image/heic-sequence',
  4352             'heics' => 'image/heic-sequence',
  4223             'eml' => 'message/rfc822',
  4353             'eml' => 'message/rfc822',
  4265      *
  4395      *
  4266      * @return string
  4396      * @return string
  4267      */
  4397      */
  4268     public static function filenameToType($filename)
  4398     public static function filenameToType($filename)
  4269     {
  4399     {
  4270         // In case the path is a URL, strip any query string before getting extension
  4400         //In case the path is a URL, strip any query string before getting extension
  4271         $qpos = strpos($filename, '?');
  4401         $qpos = strpos($filename, '?');
  4272         if (false !== $qpos) {
  4402         if (false !== $qpos) {
  4273             $filename = substr($filename, 0, $qpos);
  4403             $filename = substr($filename, 0, $qpos);
  4274         }
  4404         }
  4275         $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
  4405         $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
  4376     public static function normalizeBreaks($text, $breaktype = null)
  4506     public static function normalizeBreaks($text, $breaktype = null)
  4377     {
  4507     {
  4378         if (null === $breaktype) {
  4508         if (null === $breaktype) {
  4379             $breaktype = static::$LE;
  4509             $breaktype = static::$LE;
  4380         }
  4510         }
  4381         // Normalise to \n
  4511         //Normalise to \n
  4382         $text = str_replace([self::CRLF, "\r"], "\n", $text);
  4512         $text = str_replace([self::CRLF, "\r"], "\n", $text);
  4383         // Now convert LE as needed
  4513         //Now convert LE as needed
  4384         if ("\n" !== $breaktype) {
  4514         if ("\n" !== $breaktype) {
  4385             $text = str_replace("\n", $breaktype, $text);
  4515             $text = str_replace("\n", $breaktype, $text);
  4386         }
  4516         }
  4387 
  4517 
  4388         return $text;
  4518         return $text;
  4484             $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
  4614             $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
  4485         } else {
  4615         } else {
  4486             $privKey = openssl_pkey_get_private($privKeyStr);
  4616             $privKey = openssl_pkey_get_private($privKeyStr);
  4487         }
  4617         }
  4488         if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
  4618         if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
       
  4619             if (\PHP_MAJOR_VERSION < 8) {
       
  4620                 openssl_pkey_free($privKey);
       
  4621             }
       
  4622 
       
  4623             return base64_encode($signature);
       
  4624         }
       
  4625         if (\PHP_MAJOR_VERSION < 8) {
  4489             openssl_pkey_free($privKey);
  4626             openssl_pkey_free($privKey);
  4490 
  4627         }
  4491             return base64_encode($signature);
       
  4492         }
       
  4493         openssl_pkey_free($privKey);
       
  4494 
  4628 
  4495         return '';
  4629         return '';
  4496     }
  4630     }
  4497 
  4631 
  4498     /**
  4632     /**
  4553     public function DKIM_BodyC($body)
  4687     public function DKIM_BodyC($body)
  4554     {
  4688     {
  4555         if (empty($body)) {
  4689         if (empty($body)) {
  4556             return self::CRLF;
  4690             return self::CRLF;
  4557         }
  4691         }
  4558         // Normalize line endings to CRLF
  4692         //Normalize line endings to CRLF
  4559         $body = static::normalizeBreaks($body, self::CRLF);
  4693         $body = static::normalizeBreaks($body, self::CRLF);
  4560 
  4694 
  4561         //Reduce multiple trailing line breaks to a single one
  4695         //Reduce multiple trailing line breaks to a single one
  4562         return static::stripTrailingWSP($body) . self::CRLF;
  4696         return static::stripTrailingWSP($body) . self::CRLF;
  4563     }
  4697     }
  4573      *
  4707      *
  4574      * @return string
  4708      * @return string
  4575      */
  4709      */
  4576     public function DKIM_Add($headers_line, $subject, $body)
  4710     public function DKIM_Add($headers_line, $subject, $body)
  4577     {
  4711     {
  4578         $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms
  4712         $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
  4579         $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization methods of header & body
  4713         $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
  4580         $DKIMquery = 'dns/txt'; // Query method
  4714         $DKIMquery = 'dns/txt'; //Query method
  4581         $DKIMtime = time();
  4715         $DKIMtime = time();
  4582         //Always sign these headers without being asked
  4716         //Always sign these headers without being asked
  4583         //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
  4717         //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
  4584         $autoSignHeaders = [
  4718         $autoSignHeaders = [
  4585             'from',
  4719             'from',
  4676             $copiedHeaderFields .= ';' . static::$LE;
  4810             $copiedHeaderFields .= ';' . static::$LE;
  4677         }
  4811         }
  4678         $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
  4812         $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
  4679         $headerValues = implode(static::$LE, $headersToSign);
  4813         $headerValues = implode(static::$LE, $headersToSign);
  4680         $body = $this->DKIM_BodyC($body);
  4814         $body = $this->DKIM_BodyC($body);
  4681         $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body
  4815         //Base64 of packed binary SHA-256 hash of body
       
  4816         $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
  4682         $ident = '';
  4817         $ident = '';
  4683         if ('' !== $this->DKIM_identity) {
  4818         if ('' !== $this->DKIM_identity) {
  4684             $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
  4819             $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
  4685         }
  4820         }
  4686         //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
  4821         //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag