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 */ |
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 |
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 |
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 } |
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:;'); |
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 { |
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(); |
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 * |
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 } |
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 |
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', |
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")); |
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; |
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)) { |
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()); |
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 |
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 * ``` |
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 /** |
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 |