PHPMailer.php (PHPMailer-6.2.0) | : | PHPMailer.php (PHPMailer-6.3.0) | ||
---|---|---|---|---|
skipping to change at line 751 | skipping to change at line 751 | |||
* | * | |||
* @var string | * @var string | |||
*/ | */ | |||
protected $uniqueid = ''; | protected $uniqueid = ''; | |||
/** | /** | |||
* The PHPMailer Version number. | * The PHPMailer Version number. | |||
* | * | |||
* @var string | * @var string | |||
*/ | */ | |||
const VERSION = '6.2.0'; | const VERSION = '6.3.0'; | |||
/** | /** | |||
* Error severity: message only, continue processing. | * Error severity: message only, continue processing. | |||
* | * | |||
* @var int | * @var int | |||
*/ | */ | |||
const STOP_MESSAGE = 0; | const STOP_MESSAGE = 0; | |||
/** | /** | |||
* Error severity: message, likely ok to continue processing. | * Error severity: message, likely ok to continue processing. | |||
skipping to change at line 865 | skipping to change at line 865 | |||
*/ | */ | |||
private function mailPassthru($to, $subject, $body, $header, $params) | private function mailPassthru($to, $subject, $body, $header, $params) | |||
{ | { | |||
//Check overloading of mail function to avoid double-encoding | //Check overloading of mail function to avoid double-encoding | |||
if (ini_get('mbstring.func_overload') & 1) { | if (ini_get('mbstring.func_overload') & 1) { | |||
$subject = $this->secureHeader($subject); | $subject = $this->secureHeader($subject); | |||
} else { | } else { | |||
$subject = $this->encodeHeader($this->secureHeader($subject)); | $subject = $this->encodeHeader($this->secureHeader($subject)); | |||
} | } | |||
//Calling mail() with null params breaks | //Calling mail() with null params breaks | |||
$this->edebug('Sending with mail()'); | ||||
$this->edebug('Sendmail path: ' . ini_get('sendmail_path')); | ||||
$this->edebug("Envelope sender: {$this->Sender}"); | ||||
$this->edebug("To: {$to}"); | ||||
$this->edebug("Subject: {$subject}"); | ||||
$this->edebug("Headers: {$header}"); | ||||
if (!$this->UseSendmailOptions || null === $params) { | if (!$this->UseSendmailOptions || null === $params) { | |||
$result = @mail($to, $subject, $body, $header); | $result = @mail($to, $subject, $body, $header); | |||
} else { | } else { | |||
$this->edebug("Additional params: {$params}"); | ||||
$result = @mail($to, $subject, $body, $header, $params); | $result = @mail($to, $subject, $body, $header, $params); | |||
} | } | |||
$this->edebug('Result: ' . ($result ? 'true' : 'false')); | ||||
return $result; | return $result; | |||
} | } | |||
/** | /** | |||
* Output debugging info via user-defined method. | * Output debugging info via a user-defined method. | |||
* Only generates output if SMTP debug output is enabled (@see SMTP::$do_deb | * Only generates output if debug output is enabled. | |||
ug). | ||||
* | * | |||
* @see PHPMailer::$Debugoutput | * @see PHPMailer::$Debugoutput | |||
* @see PHPMailer::$SMTPDebug | * @see PHPMailer::$SMTPDebug | |||
* | * | |||
* @param string $str | * @param string $str | |||
*/ | */ | |||
protected function edebug($str) | protected function edebug($str) | |||
{ | { | |||
if ($this->SMTPDebug <= 0) { | if ($this->SMTPDebug <= 0) { | |||
return; | return; | |||
skipping to change at line 1073 | skipping to change at line 1080 | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return bool true on success, false if address already used or invalid in some way | * @return bool true on success, false if address already used or invalid in some way | |||
*/ | */ | |||
protected function addOrEnqueueAnAddress($kind, $address, $name) | protected function addOrEnqueueAnAddress($kind, $address, $name) | |||
{ | { | |||
$address = trim($address); | $address = trim($address); | |||
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and t rim | $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and t rim | |||
$pos = strrpos($address, '@'); | $pos = strrpos($address, '@'); | |||
if (false === $pos) { | if (false === $pos) { | |||
// At-sign is missing. | //At-sign is missing. | |||
$error_message = sprintf( | $error_message = sprintf( | |||
'%s (%s): %s', | '%s (%s): %s', | |||
$this->lang('invalid_address'), | $this->lang('invalid_address'), | |||
$kind, | $kind, | |||
$address | $address | |||
); | ); | |||
$this->setError($error_message); | $this->setError($error_message); | |||
$this->edebug($error_message); | $this->edebug($error_message); | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw new Exception($error_message); | throw new Exception($error_message); | |||
} | } | |||
return false; | return false; | |||
} | } | |||
$params = [$kind, $address, $name]; | $params = [$kind, $address, $name]; | |||
// Enqueue addresses with IDN until we know the PHPMailer::$CharSet. | //Enqueue addresses with IDN until we know the PHPMailer::$CharSet. | |||
if (static::idnSupported() && $this->has8bitChars(substr($address, ++$po s))) { | if (static::idnSupported() && $this->has8bitChars(substr($address, ++$po s))) { | |||
if ('Reply-To' !== $kind) { | if ('Reply-To' !== $kind) { | |||
if (!array_key_exists($address, $this->RecipientsQueue)) { | if (!array_key_exists($address, $this->RecipientsQueue)) { | |||
$this->RecipientsQueue[$address] = $params; | $this->RecipientsQueue[$address] = $params; | |||
return true; | return true; | |||
} | } | |||
} elseif (!array_key_exists($address, $this->ReplyToQueue)) { | } elseif (!array_key_exists($address, $this->ReplyToQueue)) { | |||
$this->ReplyToQueue[$address] = $params; | $this->ReplyToQueue[$address] = $params; | |||
return true; | return true; | |||
} | } | |||
return false; | return false; | |||
} | } | |||
// Immediately add standard addresses without IDN. | //Immediately add standard addresses without IDN. | |||
return call_user_func_array([$this, 'addAnAddress'], $params); | return call_user_func_array([$this, 'addAnAddress'], $params); | |||
} | } | |||
/** | /** | |||
* Add an address to one of the recipient arrays or to the ReplyTo array. | * Add an address to one of the recipient arrays or to the ReplyTo array. | |||
* Addresses that have been added already return false, but do not throw exc eptions. | * Addresses that have been added already return false, but do not throw exc eptions. | |||
* | * | |||
* @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' | * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' | |||
* @param string $address The email address to send, resp. to reply to | * @param string $address The email address to send, resp. to reply to | |||
* @param string $name | * @param string $name | |||
skipping to change at line 1194 | skipping to change at line 1201 | |||
$addresses = []; | $addresses = []; | |||
if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { | if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { | |||
//Use this built-in parser if it's available | //Use this built-in parser if it's available | |||
$list = imap_rfc822_parse_adrlist($addrstr, ''); | $list = imap_rfc822_parse_adrlist($addrstr, ''); | |||
foreach ($list as $address) { | foreach ($list as $address) { | |||
if ( | if ( | |||
('.SYNTAX-ERROR.' !== $address->host) && static::validateAdd ress( | ('.SYNTAX-ERROR.' !== $address->host) && static::validateAdd ress( | |||
$address->mailbox . '@' . $address->host | $address->mailbox . '@' . $address->host | |||
) | ) | |||
) { | ) { | |||
//Decode the name part if it's present and encoded | ||||
if (property_exists($address, 'personal') && preg_match('/^= | ||||
\?.*\?=$/', $address->personal)) { | ||||
$address->personal = mb_decode_mimeheader($address->pers | ||||
onal); | ||||
} | ||||
$addresses[] = [ | $addresses[] = [ | |||
'name' => (property_exists($address, 'personal') ? $addr ess->personal : ''), | 'name' => (property_exists($address, 'personal') ? $addr ess->personal : ''), | |||
'address' => $address->mailbox . '@' . $address->host, | 'address' => $address->mailbox . '@' . $address->host, | |||
]; | ]; | |||
} | } | |||
} | } | |||
} else { | } else { | |||
//Use this simpler parser | //Use this simpler parser | |||
$list = explode(',', $addrstr); | $list = explode(',', $addrstr); | |||
foreach ($list as $address) { | foreach ($list as $address) { | |||
skipping to change at line 1217 | skipping to change at line 1229 | |||
//No separate name, just use the whole thing | //No separate name, just use the whole thing | |||
if (static::validateAddress($address)) { | if (static::validateAddress($address)) { | |||
$addresses[] = [ | $addresses[] = [ | |||
'name' => '', | 'name' => '', | |||
'address' => $address, | 'address' => $address, | |||
]; | ]; | |||
} | } | |||
} else { | } else { | |||
list($name, $email) = explode('<', $address); | list($name, $email) = explode('<', $address); | |||
$email = trim(str_replace('>', '', $email)); | $email = trim(str_replace('>', '', $email)); | |||
$name = trim($name); | ||||
if (static::validateAddress($email)) { | if (static::validateAddress($email)) { | |||
//If this name is encoded, decode it | ||||
if (preg_match('/^=\?.*\?=$/', $name)) { | ||||
$name = mb_decode_mimeheader($name); | ||||
} | ||||
$addresses[] = [ | $addresses[] = [ | |||
'name' => trim(str_replace(['"', "'"], '', $name)), | //Remove any surrounding quotes and spaces from the | |||
name | ||||
'name' => trim($name, '\'" '), | ||||
'address' => $email, | 'address' => $email, | |||
]; | ]; | |||
} | } | |||
} | } | |||
} | } | |||
} | } | |||
return $addresses; | return $addresses; | |||
} | } | |||
skipping to change at line 1245 | skipping to change at line 1263 | |||
* @param bool $auto Whether to also set the Sender address, defaults t o true | * @param bool $auto Whether to also set the Sender address, defaults t o true | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
public function setFrom($address, $name = '', $auto = true) | public function setFrom($address, $name = '', $auto = true) | |||
{ | { | |||
$address = trim($address); | $address = trim($address); | |||
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and t rim | $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and t rim | |||
// Don't validate now addresses with IDN. Will be done in send(). | //Don't validate now addresses with IDN. Will be done in send(). | |||
$pos = strrpos($address, '@'); | $pos = strrpos($address, '@'); | |||
if ( | if ( | |||
(false === $pos) | (false === $pos) | |||
|| ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnS upported()) | || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnS upported()) | |||
&& !static::validateAddress($address)) | && !static::validateAddress($address)) | |||
) { | ) { | |||
$error_message = sprintf( | $error_message = sprintf( | |||
'%s (From): %s', | '%s (From): %s', | |||
$this->lang('invalid_address'), | $this->lang('invalid_address'), | |||
$address | $address | |||
skipping to change at line 1398 | skipping to change at line 1416 | |||
* or fails for any reason (e.g. domain contains characters not allowed in an IDN). | * or fails for any reason (e.g. domain contains characters not allowed in an IDN). | |||
* | * | |||
* @see PHPMailer::$CharSet | * @see PHPMailer::$CharSet | |||
* | * | |||
* @param string $address The email address to convert | * @param string $address The email address to convert | |||
* | * | |||
* @return string The encoded address in ASCII form | * @return string The encoded address in ASCII form | |||
*/ | */ | |||
public function punyencodeAddress($address) | public function punyencodeAddress($address) | |||
{ | { | |||
// Verify we have required functions, CharSet, and at-sign. | //Verify we have required functions, CharSet, and at-sign. | |||
$pos = strrpos($address, '@'); | $pos = strrpos($address, '@'); | |||
if ( | if ( | |||
!empty($this->CharSet) && | !empty($this->CharSet) && | |||
false !== $pos && | false !== $pos && | |||
static::idnSupported() | static::idnSupported() | |||
) { | ) { | |||
$domain = substr($address, ++$pos); | $domain = substr($address, ++$pos); | |||
// Verify CharSet string is a valid one, and domain properly encoded in this CharSet. | //Verify CharSet string is a valid one, and domain properly encoded in this CharSet. | |||
if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $thi s->CharSet)) { | if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $thi s->CharSet)) { | |||
$domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); | //Convert the domain from whatever charset it's in to UTF-8 | |||
$domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this | ||||
->CharSet); | ||||
//Ignore IDE complaints about this line - method signature chang ed in PHP 5.4 | //Ignore IDE complaints about this line - method signature chang ed in PHP 5.4 | |||
$errorcode = 0; | $errorcode = 0; | |||
if (defined('INTL_IDNA_VARIANT_UTS46')) { | if (defined('INTL_IDNA_VARIANT_UTS46')) { | |||
$punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARI | //Use the current punycode standard (appeared in PHP 7.2) | |||
ANT_UTS46); | $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VAR | |||
IANT_UTS46); | ||||
} elseif (defined('INTL_IDNA_VARIANT_2003')) { | } elseif (defined('INTL_IDNA_VARIANT_2003')) { | |||
$punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARI | //Fall back to this old, deprecated/removed encoding | |||
ANT_2003); | $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VAR | |||
IANT_2003); | ||||
} else { | } else { | |||
//Fall back to a default we don't know about | ||||
$punycode = idn_to_ascii($domain, $errorcode); | $punycode = idn_to_ascii($domain, $errorcode); | |||
} | } | |||
if (false !== $punycode) { | if (false !== $punycode) { | |||
return substr($address, 0, $pos) . $punycode; | return substr($address, 0, $pos) . $punycode; | |||
} | } | |||
} | } | |||
} | } | |||
return $address; | return $address; | |||
} | } | |||
skipping to change at line 1465 | skipping to change at line 1487 | |||
* Prepare a message for sending. | * Prepare a message for sending. | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
public function preSend() | public function preSend() | |||
{ | { | |||
if ( | if ( | |||
'smtp' === $this->Mailer | 'smtp' === $this->Mailer | |||
|| ('mail' === $this->Mailer && (PHP_VERSION_ID >= 80000 || stripos( PHP_OS, 'WIN') === 0)) | || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos (PHP_OS, 'WIN') === 0)) | |||
) { | ) { | |||
//SMTP mandates RFC-compliant line endings | //SMTP mandates RFC-compliant line endings | |||
//and it's also used with mail() on Windows | //and it's also used with mail() on Windows | |||
static::setLE(self::CRLF); | static::setLE(self::CRLF); | |||
} else { | } else { | |||
//Maintain backward compatibility with legacy Linux command line mai lers | //Maintain backward compatibility with legacy Linux command line mai lers | |||
static::setLE(PHP_EOL); | static::setLE(PHP_EOL); | |||
} | } | |||
//Check for buggy PHP versions that add a header with an incorrect line break | //Check for buggy PHP versions that add a header with an incorrect line break | |||
if ( | if ( | |||
'mail' === $this->Mailer | 'mail' === $this->Mailer | |||
&& ((PHP_VERSION_ID >= 70000 && PHP_VERSION_ID < 70017) | && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017) | |||
|| (PHP_VERSION_ID >= 70100 && PHP_VERSION_ID < 70103)) | || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103)) | |||
&& ini_get('mail.add_x_header') === '1' | && ini_get('mail.add_x_header') === '1' | |||
&& stripos(PHP_OS, 'WIN') === 0 | && stripos(PHP_OS, 'WIN') === 0 | |||
) { | ) { | |||
trigger_error( | trigger_error( | |||
'Your version of PHP is affected by a bug that may result in cor rupted messages.' . | 'Your version of PHP is affected by a bug that may result in cor rupted messages.' . | |||
' To fix it, switch to sending using SMTP, disable the mail.add_ x_header option in' . | ' To fix it, switch to sending using SMTP, disable the mail.add_ x_header option in' . | |||
' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', | ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', | |||
E_USER_WARNING | E_USER_WARNING | |||
); | ); | |||
} | } | |||
try { | try { | |||
$this->error_count = 0; // Reset errors | $this->error_count = 0; //Reset errors | |||
$this->mailHeader = ''; | $this->mailHeader = ''; | |||
// Dequeue recipient and Reply-To addresses with IDN | //Dequeue recipient and Reply-To addresses with IDN | |||
foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { | foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { | |||
$params[1] = $this->punyencodeAddress($params[1]); | $params[1] = $this->punyencodeAddress($params[1]); | |||
call_user_func_array([$this, 'addAnAddress'], $params); | call_user_func_array([$this, 'addAnAddress'], $params); | |||
} | } | |||
if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { | if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { | |||
throw new Exception($this->lang('provide_address'), self::STOP_C RITICAL); | throw new Exception($this->lang('provide_address'), self::STOP_C RITICAL); | |||
} | } | |||
// Validate From, Sender, and ConfirmReadingTo addresses | //Validate From, Sender, and ConfirmReadingTo addresses | |||
foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { | foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { | |||
$this->$address_kind = trim($this->$address_kind); | $this->$address_kind = trim($this->$address_kind); | |||
if (empty($this->$address_kind)) { | if (empty($this->$address_kind)) { | |||
continue; | continue; | |||
} | } | |||
$this->$address_kind = $this->punyencodeAddress($this->$address_ kind); | $this->$address_kind = $this->punyencodeAddress($this->$address_ kind); | |||
if (!static::validateAddress($this->$address_kind)) { | if (!static::validateAddress($this->$address_kind)) { | |||
$error_message = sprintf( | $error_message = sprintf( | |||
'%s (%s): %s', | '%s (%s): %s', | |||
$this->lang('invalid_address'), | $this->lang('invalid_address'), | |||
skipping to change at line 1527 | skipping to change at line 1549 | |||
$this->setError($error_message); | $this->setError($error_message); | |||
$this->edebug($error_message); | $this->edebug($error_message); | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw new Exception($error_message); | throw new Exception($error_message); | |||
} | } | |||
return false; | return false; | |||
} | } | |||
} | } | |||
// Set whether the message is multipart/alternative | //Set whether the message is multipart/alternative | |||
if ($this->alternativeExists()) { | if ($this->alternativeExists()) { | |||
$this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; | $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; | |||
} | } | |||
$this->setMessageType(); | $this->setMessageType(); | |||
// Refuse to send an empty message unless we are specifically allowi ng it | //Refuse to send an empty message unless we are specifically allowin g it | |||
if (!$this->AllowEmpty && empty($this->Body)) { | if (!$this->AllowEmpty && empty($this->Body)) { | |||
throw new Exception($this->lang('empty_message'), self::STOP_CRI TICAL); | throw new Exception($this->lang('empty_message'), self::STOP_CRI TICAL); | |||
} | } | |||
//Trim subject consistently | //Trim subject consistently | |||
$this->Subject = trim($this->Subject); | $this->Subject = trim($this->Subject); | |||
// Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) | //Create body before headers in case body makes changes to headers ( e.g. altering transfer encoding) | |||
$this->MIMEHeader = ''; | $this->MIMEHeader = ''; | |||
$this->MIMEBody = $this->createBody(); | $this->MIMEBody = $this->createBody(); | |||
// createBody may have added some headers, so retain them | //createBody may have added some headers, so retain them | |||
$tempheaders = $this->MIMEHeader; | $tempheaders = $this->MIMEHeader; | |||
$this->MIMEHeader = $this->createHeader(); | $this->MIMEHeader = $this->createHeader(); | |||
$this->MIMEHeader .= $tempheaders; | $this->MIMEHeader .= $tempheaders; | |||
// To capture the complete message when using mail(), create | //To capture the complete message when using mail(), create | |||
// an extra header list which createHeader() doesn't fold in | //an extra header list which createHeader() doesn't fold in | |||
if ('mail' === $this->Mailer) { | if ('mail' === $this->Mailer) { | |||
if (count($this->to) > 0) { | if (count($this->to) > 0) { | |||
$this->mailHeader .= $this->addrAppend('To', $this->to); | $this->mailHeader .= $this->addrAppend('To', $this->to); | |||
} else { | } else { | |||
$this->mailHeader .= $this->headerLine('To', 'undisclosed-re cipients:;'); | $this->mailHeader .= $this->headerLine('To', 'undisclosed-re cipients:;'); | |||
} | } | |||
$this->mailHeader .= $this->headerLine( | $this->mailHeader .= $this->headerLine( | |||
'Subject', | 'Subject', | |||
$this->encodeHeader($this->secureHeader($this->Subject)) | $this->encodeHeader($this->secureHeader($this->Subject)) | |||
); | ); | |||
} | } | |||
// Sign with DKIM if enabled | //Sign with DKIM if enabled | |||
if ( | if ( | |||
!empty($this->DKIM_domain) | !empty($this->DKIM_domain) | |||
&& !empty($this->DKIM_selector) | && !empty($this->DKIM_selector) | |||
&& (!empty($this->DKIM_private_string) | && (!empty($this->DKIM_private_string) | |||
|| (!empty($this->DKIM_private) | || (!empty($this->DKIM_private) | |||
&& static::isPermittedPath($this->DKIM_private) | && static::isPermittedPath($this->DKIM_private) | |||
&& file_exists($this->DKIM_private) | && file_exists($this->DKIM_private) | |||
) | ) | |||
) | ) | |||
) { | ) { | |||
skipping to change at line 1603 | skipping to change at line 1625 | |||
/** | /** | |||
* Actually send a message via the selected mechanism. | * Actually send a message via the selected mechanism. | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
public function postSend() | public function postSend() | |||
{ | { | |||
try { | try { | |||
// Choose the mailer and send through it | //Choose the mailer and send through it | |||
switch ($this->Mailer) { | switch ($this->Mailer) { | |||
case 'sendmail': | case 'sendmail': | |||
case 'qmail': | case 'qmail': | |||
return $this->sendmailSend($this->MIMEHeader, $this->MIMEBod y); | return $this->sendmailSend($this->MIMEHeader, $this->MIMEBod y); | |||
case 'smtp': | case 'smtp': | |||
return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); | return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); | |||
case 'mail': | case 'mail': | |||
return $this->mailSend($this->MIMEHeader, $this->MIMEBody); | return $this->mailSend($this->MIMEHeader, $this->MIMEBody); | |||
default: | default: | |||
$sendMethod = $this->Mailer . 'Send'; | $sendMethod = $this->Mailer . 'Send'; | |||
skipping to change at line 1648 | skipping to change at line 1670 | |||
* | * | |||
* @param string $header The message headers | * @param string $header The message headers | |||
* @param string $body The message body | * @param string $body The message body | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
protected function sendmailSend($header, $body) | protected function sendmailSend($header, $body) | |||
{ | { | |||
if ($this->Mailer === 'qmail') { | ||||
$this->edebug('Sending with qmail'); | ||||
} else { | ||||
$this->edebug('Sending with sendmail'); | ||||
} | ||||
$header = static::stripTrailingWSP($header) . static::$LE . static::$LE; | $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; | |||
//This sets the SMTP envelope sender which gets turned into a return-pat | ||||
// CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be e | h header by the receiver | |||
scaped. | //A space after `-f` is optional, but there is a long history of its pre | |||
if (!empty($this->Sender) && self::isShellSafe($this->Sender)) { | sence | |||
if ('qmail' === $this->Mailer) { | //causing problems, so we don't use one | |||
//Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch | ||||
-the_exim_command_line.html | ||||
//Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html | ||||
//Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html | ||||
//Example problem: https://www.drupal.org/node/1057954 | ||||
//CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be es | ||||
caped. | ||||
if ('' === $this->Sender) { | ||||
$this->Sender = $this->From; | ||||
} | ||||
if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) { | ||||
//PHP config has a sender address we can use | ||||
$this->Sender = ini_get('sendmail_from'); | ||||
} | ||||
//CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be es | ||||
caped. | ||||
//But sendmail requires this param, so fail without it | ||||
if (!empty($this->Sender) && static::validateAddress($this->Sender) && s | ||||
elf::isShellSafe($this->Sender)) { | ||||
if ($this->Mailer === 'qmail') { | ||||
$sendmailFmt = '%s -f%s'; | $sendmailFmt = '%s -f%s'; | |||
} else { | } else { | |||
$sendmailFmt = '%s -oi -f%s -t'; | $sendmailFmt = '%s -oi -f%s -t'; | |||
} | } | |||
} elseif ('qmail' === $this->Mailer) { | ||||
$sendmailFmt = '%s'; | ||||
} else { | } else { | |||
$sendmailFmt = '%s -oi -t'; | $this->edebug('Sender address unusable or missing: ' . $this->Sender | |||
); | ||||
return false; | ||||
} | } | |||
$sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this ->Sender); | $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this ->Sender); | |||
$this->edebug('Sendmail path: ' . $this->Sendmail); | ||||
$this->edebug('Sendmail command: ' . $sendmail); | ||||
$this->edebug('Envelope sender: ' . $this->Sender); | ||||
$this->edebug("Headers: {$header}"); | ||||
if ($this->SingleTo) { | if ($this->SingleTo) { | |||
foreach ($this->SingleToArray as $toAddr) { | foreach ($this->SingleToArray as $toAddr) { | |||
$mail = @popen($sendmail, 'w'); | $mail = @popen($sendmail, 'w'); | |||
if (!$mail) { | if (!$mail) { | |||
throw new Exception($this->lang('execute') . $this->Sendmail , self::STOP_CRITICAL); | throw new Exception($this->lang('execute') . $this->Sendmail , self::STOP_CRITICAL); | |||
} | } | |||
$this->edebug("To: {$toAddr}"); | ||||
fwrite($mail, 'To: ' . $toAddr . "\n"); | fwrite($mail, 'To: ' . $toAddr . "\n"); | |||
fwrite($mail, $header); | fwrite($mail, $header); | |||
fwrite($mail, $body); | fwrite($mail, $body); | |||
$result = pclose($mail); | $result = pclose($mail); | |||
$this->doCallback( | $this->doCallback( | |||
($result === 0), | ($result === 0), | |||
[$toAddr], | [$toAddr], | |||
$this->cc, | $this->cc, | |||
$this->bcc, | $this->bcc, | |||
$this->Subject, | $this->Subject, | |||
$body, | $body, | |||
$this->From, | $this->From, | |||
[] | [] | |||
); | ); | |||
$this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); | ||||
if (0 !== $result) { | if (0 !== $result) { | |||
throw new Exception($this->lang('execute') . $this->Sendmail , self::STOP_CRITICAL); | throw new Exception($this->lang('execute') . $this->Sendmail , self::STOP_CRITICAL); | |||
} | } | |||
} | } | |||
} else { | } else { | |||
$mail = @popen($sendmail, 'w'); | $mail = @popen($sendmail, 'w'); | |||
if (!$mail) { | if (!$mail) { | |||
throw new Exception($this->lang('execute') . $this->Sendmail, se lf::STOP_CRITICAL); | throw new Exception($this->lang('execute') . $this->Sendmail, se lf::STOP_CRITICAL); | |||
} | } | |||
fwrite($mail, $header); | fwrite($mail, $header); | |||
skipping to change at line 1707 | skipping to change at line 1754 | |||
$this->doCallback( | $this->doCallback( | |||
($result === 0), | ($result === 0), | |||
$this->to, | $this->to, | |||
$this->cc, | $this->cc, | |||
$this->bcc, | $this->bcc, | |||
$this->Subject, | $this->Subject, | |||
$body, | $body, | |||
$this->From, | $this->From, | |||
[] | [] | |||
); | ); | |||
$this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); | ||||
if (0 !== $result) { | if (0 !== $result) { | |||
throw new Exception($this->lang('execute') . $this->Sendmail, se lf::STOP_CRITICAL); | throw new Exception($this->lang('execute') . $this->Sendmail, se lf::STOP_CRITICAL); | |||
} | } | |||
} | } | |||
return true; | return true; | |||
} | } | |||
/** | /** | |||
* Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe s hell characters. | * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe s hell characters. | |||
* Note that escapeshellarg and escapeshellcmd are inadequate for our purpos es, especially on Windows. | * Note that escapeshellarg and escapeshellcmd are inadequate for our purpos es, especially on Windows. | |||
* | * | |||
* @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report | * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report | |||
* | * | |||
* @param string $string The string to be validated | * @param string $string The string to be validated | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
protected static function isShellSafe($string) | protected static function isShellSafe($string) | |||
{ | { | |||
// Future-proof | //Future-proof | |||
if ( | if ( | |||
escapeshellcmd($string) !== $string | escapeshellcmd($string) !== $string | |||
|| !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) | || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) | |||
) { | ) { | |||
return false; | return false; | |||
} | } | |||
$length = strlen($string); | $length = strlen($string); | |||
for ($i = 0; $i < $length; ++$i) { | for ($i = 0; $i < $length; ++$i) { | |||
$c = $string[$i]; | $c = $string[$i]; | |||
// All other characters have a special meaning in at least one commo | //All other characters have a special meaning in at least one common | |||
n shell, including = and +. | shell, including = and +. | |||
// Full stop (.) has a special meaning in cmd.exe, but its impact sh | //Full stop (.) has a special meaning in cmd.exe, but its impact sho | |||
ould be negligible here. | uld be negligible here. | |||
// Note that this does permit non-Latin alphanumeric characters base | //Note that this does permit non-Latin alphanumeric characters based | |||
d on the current locale. | on the current locale. | |||
if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { | if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { | |||
return false; | return false; | |||
} | } | |||
} | } | |||
return true; | return true; | |||
} | } | |||
/** | /** | |||
* Check whether a file path is of a permitted type. | * Check whether a file path is of a permitted type. | |||
skipping to change at line 1812 | skipping to change at line 1860 | |||
$to = implode(', ', $toArr); | $to = implode(', ', $toArr); | |||
$params = null; | $params = null; | |||
//This sets the SMTP envelope sender which gets turned into a return-pat h header by the receiver | //This sets the SMTP envelope sender which gets turned into a return-pat h header by the receiver | |||
//A space after `-f` is optional, but there is a long history of its pre sence | //A space after `-f` is optional, but there is a long history of its pre sence | |||
//causing problems, so we don't use one | //causing problems, so we don't use one | |||
//Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch -the_exim_command_line.html | //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch -the_exim_command_line.html | |||
//Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html | //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html | |||
//Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html | //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html | |||
//Example problem: https://www.drupal.org/node/1057954 | //Example problem: https://www.drupal.org/node/1057954 | |||
// CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be e | //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be es | |||
scaped. | caped. | |||
if (!empty($this->Sender) && static::validateAddress($this->Sender) && s | if ('' === $this->Sender) { | |||
elf::isShellSafe($this->Sender)) { | $this->Sender = $this->From; | |||
$params = sprintf('-f%s', $this->Sender); | } | |||
if (empty($this->Sender) && !empty(ini_get('sendmail_from'))) { | ||||
//PHP config has a sender address we can use | ||||
$this->Sender = ini_get('sendmail_from'); | ||||
} | } | |||
if (!empty($this->Sender) && static::validateAddress($this->Sender)) { | if (!empty($this->Sender) && static::validateAddress($this->Sender)) { | |||
if (self::isShellSafe($this->Sender)) { | ||||
$params = sprintf('-f%s', $this->Sender); | ||||
} | ||||
$old_from = ini_get('sendmail_from'); | $old_from = ini_get('sendmail_from'); | |||
ini_set('sendmail_from', $this->Sender); | ini_set('sendmail_from', $this->Sender); | |||
} | } | |||
$result = false; | $result = false; | |||
if ($this->SingleTo && count($toArr) > 1) { | if ($this->SingleTo && count($toArr) > 1) { | |||
foreach ($toArr as $toAddr) { | foreach ($toArr as $toAddr) { | |||
$result = $this->mailPassthru($toAddr, $this->Subject, $body, $h eader, $params); | $result = $this->mailPassthru($toAddr, $this->Subject, $body, $h eader, $params); | |||
$this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $th is->Subject, $body, $this->From, []); | $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $th is->Subject, $body, $this->From, []); | |||
} | } | |||
} else { | } else { | |||
skipping to change at line 1902 | skipping to change at line 1957 | |||
$smtp_from = $this->From; | $smtp_from = $this->From; | |||
} else { | } else { | |||
$smtp_from = $this->Sender; | $smtp_from = $this->Sender; | |||
} | } | |||
if (!$this->smtp->mail($smtp_from)) { | if (!$this->smtp->mail($smtp_from)) { | |||
$this->setError($this->lang('from_failed') . $smtp_from . ' : ' . im plode(',', $this->smtp->getError())); | $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . im plode(',', $this->smtp->getError())); | |||
throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); | throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); | |||
} | } | |||
$callbacks = []; | $callbacks = []; | |||
// Attempt to send to all recipients | //Attempt to send to all recipients | |||
foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { | foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { | |||
foreach ($togroup as $to) { | foreach ($togroup as $to) { | |||
if (!$this->smtp->recipient($to[0], $this->dsn)) { | if (!$this->smtp->recipient($to[0], $this->dsn)) { | |||
$error = $this->smtp->getError(); | $error = $this->smtp->getError(); | |||
$bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; | $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; | |||
$isSent = false; | $isSent = false; | |||
} else { | } else { | |||
$isSent = true; | $isSent = true; | |||
} | } | |||
$callbacks[] = ['issent' => $isSent, 'to' => $to[0]]; | $callbacks[] = ['issent' => $isSent, 'to' => $to[0]]; | |||
} | } | |||
} | } | |||
// Only send the DATA command if we have viable recipients | //Only send the DATA command if we have viable recipients | |||
if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->d ata($header . $body)) { | if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->d ata($header . $body)) { | |||
throw new Exception($this->lang('data_not_accepted'), self::STOP_CRI TICAL); | throw new Exception($this->lang('data_not_accepted'), self::STOP_CRI TICAL); | |||
} | } | |||
$smtp_transaction_id = $this->smtp->getLastTransactionID(); | $smtp_transaction_id = $this->smtp->getLastTransactionID(); | |||
if ($this->SMTPKeepAlive) { | if ($this->SMTPKeepAlive) { | |||
$this->smtp->reset(); | $this->smtp->reset(); | |||
} else { | } else { | |||
$this->smtp->quit(); | $this->smtp->quit(); | |||
skipping to change at line 1979 | skipping to change at line 2034 | |||
{ | { | |||
if (null === $this->smtp) { | if (null === $this->smtp) { | |||
$this->smtp = $this->getSMTPInstance(); | $this->smtp = $this->getSMTPInstance(); | |||
} | } | |||
//If no options are provided, use whatever is set in the instance | //If no options are provided, use whatever is set in the instance | |||
if (null === $options) { | if (null === $options) { | |||
$options = $this->SMTPOptions; | $options = $this->SMTPOptions; | |||
} | } | |||
// Already connected? | //Already connected? | |||
if ($this->smtp->connected()) { | if ($this->smtp->connected()) { | |||
return true; | return true; | |||
} | } | |||
$this->smtp->setTimeout($this->Timeout); | $this->smtp->setTimeout($this->Timeout); | |||
$this->smtp->setDebugLevel($this->SMTPDebug); | $this->smtp->setDebugLevel($this->SMTPDebug); | |||
$this->smtp->setDebugOutput($this->Debugoutput); | $this->smtp->setDebugOutput($this->Debugoutput); | |||
$this->smtp->setVerp($this->do_verp); | $this->smtp->setVerp($this->do_verp); | |||
$hosts = explode(';', $this->Host); | $hosts = explode(';', $this->Host); | |||
$lastexception = null; | $lastexception = null; | |||
skipping to change at line 2001 | skipping to change at line 2056 | |||
foreach ($hosts as $hostentry) { | foreach ($hosts as $hostentry) { | |||
$hostinfo = []; | $hostinfo = []; | |||
if ( | if ( | |||
!preg_match( | !preg_match( | |||
'/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/', | '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/', | |||
trim($hostentry), | trim($hostentry), | |||
$hostinfo | $hostinfo | |||
) | ) | |||
) { | ) { | |||
$this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hos tentry)); | $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hos tentry)); | |||
// Not a valid host entry | //Not a valid host entry | |||
continue; | continue; | |||
} | } | |||
// $hostinfo[1]: optional ssl or tls prefix | //$hostinfo[1]: optional ssl or tls prefix | |||
// $hostinfo[2]: the hostname | //$hostinfo[2]: the hostname | |||
// $hostinfo[3]: optional port number | //$hostinfo[3]: optional port number | |||
// The host string prefix can temporarily override the current setti | //The host string prefix can temporarily override the current settin | |||
ng for SMTPSecure | g for SMTPSecure | |||
// If it's not specified, the default value is used | //If it's not specified, the default value is used | |||
//Check the host name is a valid name or IP address before trying to use it | //Check the host name is a valid name or IP address before trying to use it | |||
if (!static::isValidHost($hostinfo[2])) { | if (!static::isValidHost($hostinfo[2])) { | |||
$this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); | $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); | |||
continue; | continue; | |||
} | } | |||
$prefix = ''; | $prefix = ''; | |||
$secure = $this->SMTPSecure; | $secure = $this->SMTPSecure; | |||
$tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); | $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); | |||
if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYP TION_SMTPS === $this->SMTPSecure)) { | if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYP TION_SMTPS === $this->SMTPSecure)) { | |||
$prefix = 'ssl://'; | $prefix = 'ssl://'; | |||
$tls = false; // Can't have SSL and TLS at the same time | $tls = false; //Can't have SSL and TLS at the same time | |||
$secure = static::ENCRYPTION_SMTPS; | $secure = static::ENCRYPTION_SMTPS; | |||
} elseif ('tls' === $hostinfo[1]) { | } elseif ('tls' === $hostinfo[1]) { | |||
$tls = true; | $tls = true; | |||
// tls doesn't use a prefix | //TLS doesn't use a prefix | |||
$secure = static::ENCRYPTION_STARTTLS; | $secure = static::ENCRYPTION_STARTTLS; | |||
} | } | |||
//Do we need the OpenSSL extension? | //Do we need the OpenSSL extension? | |||
$sslext = defined('OPENSSL_ALGO_SHA256'); | $sslext = defined('OPENSSL_ALGO_SHA256'); | |||
if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SM TPS === $secure) { | if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SM TPS === $secure) { | |||
//Check for an OpenSSL constant rather than using extension_load ed, which is sometimes disabled | //Check for an OpenSSL constant rather than using extension_load ed, which is sometimes disabled | |||
if (!$sslext) { | if (!$sslext) { | |||
throw new Exception($this->lang('extension_missing') . 'open ssl', self::STOP_CRITICAL); | throw new Exception($this->lang('extension_missing') . 'open ssl', self::STOP_CRITICAL); | |||
} | } | |||
} | } | |||
skipping to change at line 2054 | skipping to change at line 2109 | |||
} | } | |||
if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $op tions)) { | if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $op tions)) { | |||
try { | try { | |||
if ($this->Helo) { | if ($this->Helo) { | |||
$hello = $this->Helo; | $hello = $this->Helo; | |||
} else { | } else { | |||
$hello = $this->serverHostname(); | $hello = $this->serverHostname(); | |||
} | } | |||
$this->smtp->hello($hello); | $this->smtp->hello($hello); | |||
//Automatically enable TLS encryption if: | //Automatically enable TLS encryption if: | |||
// * it's not disabled | //* it's not disabled | |||
// * we have openssl extension | //* we have openssl extension | |||
// * we are not already using SSL | //* we are not already using SSL | |||
// * the server offers STARTTLS | //* the server offers STARTTLS | |||
if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $t his->smtp->getServerExt('STARTTLS')) { | if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $t his->smtp->getServerExt('STARTTLS')) { | |||
$tls = true; | $tls = true; | |||
} | } | |||
if ($tls) { | if ($tls) { | |||
if (!$this->smtp->startTLS()) { | if (!$this->smtp->startTLS()) { | |||
throw new Exception($this->lang('connect_host')); | throw new Exception($this->lang('connect_host')); | |||
} | } | |||
// We must resend EHLO after TLS negotiation | //We must resend EHLO after TLS negotiation | |||
$this->smtp->hello($hello); | $this->smtp->hello($hello); | |||
} | } | |||
if ( | if ( | |||
$this->SMTPAuth && !$this->smtp->authenticate( | $this->SMTPAuth && !$this->smtp->authenticate( | |||
$this->Username, | $this->Username, | |||
$this->Password, | $this->Password, | |||
$this->AuthType, | $this->AuthType, | |||
$this->oauth | $this->oauth | |||
) | ) | |||
) { | ) { | |||
throw new Exception($this->lang('authenticate')); | throw new Exception($this->lang('authenticate')); | |||
} | } | |||
return true; | return true; | |||
} catch (Exception $exc) { | } catch (Exception $exc) { | |||
$lastexception = $exc; | $lastexception = $exc; | |||
$this->edebug($exc->getMessage()); | $this->edebug($exc->getMessage()); | |||
// We must have connected, but then failed TLS or Auth, so c lose connection nicely | //We must have connected, but then failed TLS or Auth, so cl ose connection nicely | |||
$this->smtp->quit(); | $this->smtp->quit(); | |||
} | } | |||
} | } | |||
} | } | |||
// If we get here, all connection attempts have failed, so close connect ion hard | //If we get here, all connection attempts have failed, so close connecti on hard | |||
$this->smtp->close(); | $this->smtp->close(); | |||
// As we've caught all exceptions, just report whatever the last one was | //As we've caught all exceptions, just report whatever the last one was | |||
if ($this->exceptions && null !== $lastexception) { | if ($this->exceptions && null !== $lastexception) { | |||
throw $lastexception; | throw $lastexception; | |||
} | } | |||
return false; | return false; | |||
} | } | |||
/** | /** | |||
* Close the active SMTP session if one exists. | * Close the active SMTP session if one exists. | |||
*/ | */ | |||
skipping to change at line 2121 | skipping to change at line 2176 | |||
* Returns false if it cannot load the language file. | * Returns false if it cannot load the language file. | |||
* The default language is English. | * The default language is English. | |||
* | * | |||
* @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") | * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") | |||
* @param string $lang_path Path to the language file directory, with traili ng separator (slash) | * @param string $lang_path Path to the language file directory, with traili ng separator (slash) | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
public function setLanguage($langcode = 'en', $lang_path = '') | public function setLanguage($langcode = 'en', $lang_path = '') | |||
{ | { | |||
// Backwards compatibility for renamed language codes | //Backwards compatibility for renamed language codes | |||
$renamed_langcodes = [ | $renamed_langcodes = [ | |||
'br' => 'pt_br', | 'br' => 'pt_br', | |||
'cz' => 'cs', | 'cz' => 'cs', | |||
'dk' => 'da', | 'dk' => 'da', | |||
'no' => 'nb', | 'no' => 'nb', | |||
'se' => 'sv', | 'se' => 'sv', | |||
'rs' => 'sr', | 'rs' => 'sr', | |||
'tg' => 'tl', | 'tg' => 'tl', | |||
'am' => 'hy', | 'am' => 'hy', | |||
]; | ]; | |||
if (array_key_exists($langcode, $renamed_langcodes)) { | if (array_key_exists($langcode, $renamed_langcodes)) { | |||
$langcode = $renamed_langcodes[$langcode]; | $langcode = $renamed_langcodes[$langcode]; | |||
} | } | |||
// Define full set of translatable strings in English | //Define full set of translatable strings in English | |||
$PHPMAILER_LANG = [ | $PHPMAILER_LANG = [ | |||
'authenticate' => 'SMTP Error: Could not authenticate.', | 'authenticate' => 'SMTP Error: Could not authenticate.', | |||
'connect_host' => 'SMTP Error: Could not connect to SMTP host.', | 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', | |||
'data_not_accepted' => 'SMTP Error: data not accepted.', | 'data_not_accepted' => 'SMTP Error: data not accepted.', | |||
'empty_message' => 'Message body empty', | 'empty_message' => 'Message body empty', | |||
'encoding' => 'Unknown encoding: ', | 'encoding' => 'Unknown encoding: ', | |||
'execute' => 'Could not execute: ', | 'execute' => 'Could not execute: ', | |||
'file_access' => 'Could not access file: ', | 'file_access' => 'Could not access file: ', | |||
'file_open' => 'File Error: Could not open file: ', | 'file_open' => 'File Error: Could not open file: ', | |||
'from_failed' => 'The following From address failed: ', | 'from_failed' => 'The following From address failed: ', | |||
skipping to change at line 2162 | skipping to change at line 2217 | |||
'mailer_not_supported' => ' mailer is not supported.', | 'mailer_not_supported' => ' mailer is not supported.', | |||
'provide_address' => 'You must provide at least one recipient email address.', | 'provide_address' => 'You must provide at least one recipient email address.', | |||
'recipients_failed' => 'SMTP Error: The following recipients failed: ', | 'recipients_failed' => 'SMTP Error: The following recipients failed: ', | |||
'signing' => 'Signing Error: ', | 'signing' => 'Signing Error: ', | |||
'smtp_connect_failed' => 'SMTP connect() failed.', | 'smtp_connect_failed' => 'SMTP connect() failed.', | |||
'smtp_error' => 'SMTP server error: ', | 'smtp_error' => 'SMTP server error: ', | |||
'variable_set' => 'Cannot set or reset variable: ', | 'variable_set' => 'Cannot set or reset variable: ', | |||
'extension_missing' => 'Extension missing: ', | 'extension_missing' => 'Extension missing: ', | |||
]; | ]; | |||
if (empty($lang_path)) { | if (empty($lang_path)) { | |||
// Calculate an absolute path so it can work if CWD is not here | //Calculate an absolute path so it can work if CWD is not here | |||
$lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . D IRECTORY_SEPARATOR; | $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . D IRECTORY_SEPARATOR; | |||
} | } | |||
//Validate $langcode | //Validate $langcode | |||
if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { | if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { | |||
$langcode = 'en'; | $langcode = 'en'; | |||
} | } | |||
$foundlang = true; | $foundlang = true; | |||
$lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; | $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; | |||
// There is no English translation file | //There is no English translation file | |||
if ('en' !== $langcode) { | if ('en' !== $langcode) { | |||
// Make sure language file path is readable | //Make sure language file path is readable | |||
if (!static::fileIsAccessible($lang_file)) { | if (!static::fileIsAccessible($lang_file)) { | |||
$foundlang = false; | $foundlang = false; | |||
} else { | } else { | |||
// Overwrite language-specific strings. | //Overwrite language-specific strings. | |||
// This way we'll never have missing translation keys. | //This way we'll never have missing translation keys. | |||
$foundlang = include $lang_file; | $foundlang = include $lang_file; | |||
} | } | |||
} | } | |||
$this->language = $PHPMAILER_LANG; | $this->language = $PHPMAILER_LANG; | |||
return (bool) $foundlang; // Returns false if language not found | return (bool) $foundlang; //Returns false if language not found | |||
} | } | |||
/** | /** | |||
* Get the array of strings for the current language. | * Get the array of strings for the current language. | |||
* | * | |||
* @return array | * @return array | |||
*/ | */ | |||
public function getTranslations() | public function getTranslations() | |||
{ | { | |||
return $this->language; | return $this->language; | |||
skipping to change at line 2228 | skipping to change at line 2283 | |||
/** | /** | |||
* Format an address for use in a message header. | * Format an address for use in a message header. | |||
* | * | |||
* @param array $addr A 2-element indexed array, element 0 containing an add ress, element 1 containing a name like | * @param array $addr A 2-element indexed array, element 0 containing an add ress, element 1 containing a name like | |||
* ['joe@example.com', 'Joe User'] | * ['joe@example.com', 'Joe User'] | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public function addrFormat($addr) | public function addrFormat($addr) | |||
{ | { | |||
if (empty($addr[1])) { // No name provided | if (empty($addr[1])) { //No name provided | |||
return $this->secureHeader($addr[0]); | return $this->secureHeader($addr[0]); | |||
} | } | |||
return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . | return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . | |||
' <' . $this->secureHeader($addr[0]) . '>'; | ' <' . $this->secureHeader($addr[0]) . '>'; | |||
} | } | |||
/** | /** | |||
* Word-wrap message. | * Word-wrap message. | |||
* For use with mailers that do not automatically perform wrapping | * For use with mailers that do not automatically perform wrapping | |||
skipping to change at line 2255 | skipping to change at line 2310 | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public function wrapText($message, $length, $qp_mode = false) | public function wrapText($message, $length, $qp_mode = false) | |||
{ | { | |||
if ($qp_mode) { | if ($qp_mode) { | |||
$soft_break = sprintf(' =%s', static::$LE); | $soft_break = sprintf(' =%s', static::$LE); | |||
} else { | } else { | |||
$soft_break = static::$LE; | $soft_break = static::$LE; | |||
} | } | |||
// If utf-8 encoding is used, we will need to make sure we don't | //If utf-8 encoding is used, we will need to make sure we don't | |||
// split multibyte characters when we wrap | //split multibyte characters when we wrap | |||
$is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); | $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); | |||
$lelen = strlen(static::$LE); | $lelen = strlen(static::$LE); | |||
$crlflen = strlen(static::$LE); | $crlflen = strlen(static::$LE); | |||
$message = static::normalizeBreaks($message); | $message = static::normalizeBreaks($message); | |||
//Remove a trailing line break | //Remove a trailing line break | |||
if (substr($message, -$lelen) === static::$LE) { | if (substr($message, -$lelen) === static::$LE) { | |||
$message = substr($message, 0, -$lelen); | $message = substr($message, 0, -$lelen); | |||
} | } | |||
skipping to change at line 2356 | skipping to change at line 2411 | |||
* @return int | * @return int | |||
*/ | */ | |||
public function utf8CharBoundary($encodedText, $maxLength) | public function utf8CharBoundary($encodedText, $maxLength) | |||
{ | { | |||
$foundSplitPos = false; | $foundSplitPos = false; | |||
$lookBack = 3; | $lookBack = 3; | |||
while (!$foundSplitPos) { | while (!$foundSplitPos) { | |||
$lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack) ; | $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack) ; | |||
$encodedCharPos = strpos($lastChunk, '='); | $encodedCharPos = strpos($lastChunk, '='); | |||
if (false !== $encodedCharPos) { | if (false !== $encodedCharPos) { | |||
// Found start of encoded character byte within $lookBack block. | //Found start of encoded character byte within $lookBack block. | |||
// Check the encoded byte value (the 2 chars after the '=') | //Check the encoded byte value (the 2 chars after the '=') | |||
$hex = substr($encodedText, $maxLength - $lookBack + $encodedCha rPos + 1, 2); | $hex = substr($encodedText, $maxLength - $lookBack + $encodedCha rPos + 1, 2); | |||
$dec = hexdec($hex); | $dec = hexdec($hex); | |||
if ($dec < 128) { | if ($dec < 128) { | |||
// Single byte character. | //Single byte character. | |||
// If the encoded char was found at pos 0, it will fit | //If the encoded char was found at pos 0, it will fit | |||
// otherwise reduce maxLength to start of the encoded char | //otherwise reduce maxLength to start of the encoded char | |||
if ($encodedCharPos > 0) { | if ($encodedCharPos > 0) { | |||
$maxLength -= $lookBack - $encodedCharPos; | $maxLength -= $lookBack - $encodedCharPos; | |||
} | } | |||
$foundSplitPos = true; | $foundSplitPos = true; | |||
} elseif ($dec >= 192) { | } elseif ($dec >= 192) { | |||
// First byte of a multi byte character | //First byte of a multi byte character | |||
// Reduce maxLength to split at start of character | //Reduce maxLength to split at start of character | |||
$maxLength -= $lookBack - $encodedCharPos; | $maxLength -= $lookBack - $encodedCharPos; | |||
$foundSplitPos = true; | $foundSplitPos = true; | |||
} elseif ($dec < 192) { | } elseif ($dec < 192) { | |||
// Middle byte of a multi byte character, look further back | //Middle byte of a multi byte character, look further back | |||
$lookBack += 3; | $lookBack += 3; | |||
} | } | |||
} else { | } else { | |||
// No encoded character found | //No encoded character found | |||
$foundSplitPos = true; | $foundSplitPos = true; | |||
} | } | |||
} | } | |||
return $maxLength; | return $maxLength; | |||
} | } | |||
/** | /** | |||
* Apply word wrapping to the message body. | * Apply word wrapping to the message body. | |||
* Wraps the message body to the number of chars set in the WordWrap propert y. | * Wraps the message body to the number of chars set in the WordWrap propert y. | |||
skipping to change at line 2422 | skipping to change at line 2477 | |||
* Assemble message headers. | * Assemble message headers. | |||
* | * | |||
* @return string The assembled headers | * @return string The assembled headers | |||
*/ | */ | |||
public function createHeader() | public function createHeader() | |||
{ | { | |||
$result = ''; | $result = ''; | |||
$result .= $this->headerLine('Date', '' === $this->MessageDate ? self::r fcDate() : $this->MessageDate); | $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::r fcDate() : $this->MessageDate); | |||
// The To header is created automatically by mail(), so needs to be omit ted here | //The To header is created automatically by mail(), so needs to be omitt ed here | |||
if ('mail' !== $this->Mailer) { | if ('mail' !== $this->Mailer) { | |||
if ($this->SingleTo) { | if ($this->SingleTo) { | |||
foreach ($this->to as $toaddr) { | foreach ($this->to as $toaddr) { | |||
$this->SingleToArray[] = $this->addrFormat($toaddr); | $this->SingleToArray[] = $this->addrFormat($toaddr); | |||
} | } | |||
} elseif (count($this->to) > 0) { | } elseif (count($this->to) > 0) { | |||
$result .= $this->addrAppend('To', $this->to); | $result .= $this->addrAppend('To', $this->to); | |||
} elseif (count($this->cc) === 0) { | } elseif (count($this->cc) === 0) { | |||
$result .= $this->headerLine('To', 'undisclosed-recipients:;'); | $result .= $this->headerLine('To', 'undisclosed-recipients:;'); | |||
} | } | |||
} | } | |||
$result .= $this->addrAppend('From', [[trim($this->From), $this->FromNam e]]); | $result .= $this->addrAppend('From', [[trim($this->From), $this->FromNam e]]); | |||
// sendmail and mail() extract Cc from the header before sending | //sendmail and mail() extract Cc from the header before sending | |||
if (count($this->cc) > 0) { | if (count($this->cc) > 0) { | |||
$result .= $this->addrAppend('Cc', $this->cc); | $result .= $this->addrAppend('Cc', $this->cc); | |||
} | } | |||
// sendmail and mail() extract Bcc from the header before sending | //sendmail and mail() extract Bcc from the header before sending | |||
if ( | if ( | |||
( | ( | |||
'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'ma il' === $this->Mailer | 'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'ma il' === $this->Mailer | |||
) | ) | |||
&& count($this->bcc) > 0 | && count($this->bcc) > 0 | |||
) { | ) { | |||
$result .= $this->addrAppend('Bcc', $this->bcc); | $result .= $this->addrAppend('Bcc', $this->bcc); | |||
} | } | |||
if (count($this->ReplyTo) > 0) { | if (count($this->ReplyTo) > 0) { | |||
$result .= $this->addrAppend('Reply-To', $this->ReplyTo); | $result .= $this->addrAppend('Reply-To', $this->ReplyTo); | |||
} | } | |||
// mail() sets the subject itself | //mail() sets the subject itself | |||
if ('mail' !== $this->Mailer) { | if ('mail' !== $this->Mailer) { | |||
$result .= $this->headerLine('Subject', $this->encodeHeader($this->s ecureHeader($this->Subject))); | $result .= $this->headerLine('Subject', $this->encodeHeader($this->s ecureHeader($this->Subject))); | |||
} | } | |||
// Only allow a custom message ID if it conforms to RFC 5322 section 3.6 | //Only allow a custom message ID if it conforms to RFC 5322 section 3.6. | |||
.4 | 4 | |||
// https://tools.ietf.org/html/rfc5322#section-3.6.4 | //https://tools.ietf.org/html/rfc5322#section-3.6.4 | |||
if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageI D)) { | if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageI D)) { | |||
$this->lastMessageID = $this->MessageID; | $this->lastMessageID = $this->MessageID; | |||
} else { | } else { | |||
$this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->se rverHostname()); | $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->se rverHostname()); | |||
} | } | |||
$result .= $this->headerLine('Message-ID', $this->lastMessageID); | $result .= $this->headerLine('Message-ID', $this->lastMessageID); | |||
if (null !== $this->Priority) { | if (null !== $this->Priority) { | |||
$result .= $this->headerLine('X-Priority', $this->Priority); | $result .= $this->headerLine('X-Priority', $this->Priority); | |||
} | } | |||
if ('' === $this->XMailer) { | if ('' === $this->XMailer) { | |||
skipping to change at line 2487 | skipping to change at line 2542 | |||
$myXmailer = trim($this->XMailer); | $myXmailer = trim($this->XMailer); | |||
if ($myXmailer) { | if ($myXmailer) { | |||
$result .= $this->headerLine('X-Mailer', $myXmailer); | $result .= $this->headerLine('X-Mailer', $myXmailer); | |||
} | } | |||
} | } | |||
if ('' !== $this->ConfirmReadingTo) { | if ('' !== $this->ConfirmReadingTo) { | |||
$result .= $this->headerLine('Disposition-Notification-To', '<' . $t his->ConfirmReadingTo . '>'); | $result .= $this->headerLine('Disposition-Notification-To', '<' . $t his->ConfirmReadingTo . '>'); | |||
} | } | |||
// Add custom headers | //Add custom headers | |||
foreach ($this->CustomHeader as $header) { | foreach ($this->CustomHeader as $header) { | |||
$result .= $this->headerLine( | $result .= $this->headerLine( | |||
trim($header[0]), | trim($header[0]), | |||
$this->encodeHeader(trim($header[1])) | $this->encodeHeader(trim($header[1])) | |||
); | ); | |||
} | } | |||
if (!$this->sign_key_file) { | if (!$this->sign_key_file) { | |||
$result .= $this->headerLine('MIME-Version', '1.0'); | $result .= $this->headerLine('MIME-Version', '1.0'); | |||
$result .= $this->getMailMIME(); | $result .= $this->getMailMIME(); | |||
} | } | |||
skipping to change at line 2529 | skipping to change at line 2584 | |||
case 'alt_inline_attach': | case 'alt_inline_attach': | |||
$result .= $this->headerLine('Content-Type', static::CONTENT_TYP E_MULTIPART_MIXED . ';'); | $result .= $this->headerLine('Content-Type', static::CONTENT_TYP E_MULTIPART_MIXED . ';'); | |||
$result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); | $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); | |||
break; | break; | |||
case 'alt': | case 'alt': | |||
case 'alt_inline': | case 'alt_inline': | |||
$result .= $this->headerLine('Content-Type', static::CONTENT_TYP E_MULTIPART_ALTERNATIVE . ';'); | $result .= $this->headerLine('Content-Type', static::CONTENT_TYP E_MULTIPART_ALTERNATIVE . ';'); | |||
$result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); | $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); | |||
break; | break; | |||
default: | default: | |||
// Catches case 'plain': and case '': | //Catches case 'plain': and case '': | |||
$result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); | $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); | |||
$ismultipart = false; | $ismultipart = false; | |||
break; | break; | |||
} | } | |||
// RFC1341 part 5 says 7bit is assumed if not specified | //RFC1341 part 5 says 7bit is assumed if not specified | |||
if (static::ENCODING_7BIT !== $this->Encoding) { | if (static::ENCODING_7BIT !== $this->Encoding) { | |||
// RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE | //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE | |||
if ($ismultipart) { | if ($ismultipart) { | |||
if (static::ENCODING_8BIT === $this->Encoding) { | if (static::ENCODING_8BIT === $this->Encoding) { | |||
$result .= $this->headerLine('Content-Transfer-Encoding', st atic::ENCODING_8BIT); | $result .= $this->headerLine('Content-Transfer-Encoding', st atic::ENCODING_8BIT); | |||
} | } | |||
// The only remaining alternatives are quoted-printable and base 64, which are both 7bit compatible | //The only remaining alternatives are quoted-printable and base6 4, which are both 7bit compatible | |||
} else { | } else { | |||
$result .= $this->headerLine('Content-Transfer-Encoding', $this- >Encoding); | $result .= $this->headerLine('Content-Transfer-Encoding', $this- >Encoding); | |||
} | } | |||
} | } | |||
if ('mail' !== $this->Mailer) { | ||||
// $result .= static::$LE; | ||||
} | ||||
return $result; | return $result; | |||
} | } | |||
/** | /** | |||
* Returns the whole MIME message. | * Returns the whole MIME message. | |||
* Includes complete headers and body. | * Includes complete headers and body. | |||
* Only valid post preSend(). | * Only valid post preSend(). | |||
* | * | |||
* @see PHPMailer::preSend() | * @see PHPMailer::preSend() | |||
* | * | |||
skipping to change at line 2819 | skipping to change at line 2870 | |||
); | ); | |||
$body .= $this->encodeString($this->Body, $bodyEncoding); | $body .= $this->encodeString($this->Body, $bodyEncoding); | |||
$body .= static::$LE; | $body .= static::$LE; | |||
$body .= $this->attachAll('inline', $this->boundary[3]); | $body .= $this->attachAll('inline', $this->boundary[3]); | |||
$body .= static::$LE; | $body .= static::$LE; | |||
$body .= $this->endBoundary($this->boundary[2]); | $body .= $this->endBoundary($this->boundary[2]); | |||
$body .= static::$LE; | $body .= static::$LE; | |||
$body .= $this->attachAll('attachment', $this->boundary[1]); | $body .= $this->attachAll('attachment', $this->boundary[1]); | |||
break; | break; | |||
default: | default: | |||
// Catch case 'plain' and case '', applies to simple `text/plain ` and `text/html` body content types | //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types | |||
//Reset the `Encoding` property in case we changed it for line l ength reasons | //Reset the `Encoding` property in case we changed it for line l ength reasons | |||
$this->Encoding = $bodyEncoding; | $this->Encoding = $bodyEncoding; | |||
$body .= $this->encodeString($this->Body, $this->Encoding); | $body .= $this->encodeString($this->Body, $this->Encoding); | |||
break; | break; | |||
} | } | |||
if ($this->isError()) { | if ($this->isError()) { | |||
$body = ''; | $body = ''; | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw new Exception($this->lang('empty_message'), self::STOP_CRI TICAL); | throw new Exception($this->lang('empty_message'), self::STOP_CRI TICAL); | |||
skipping to change at line 2910 | skipping to change at line 2961 | |||
} | } | |||
if ('' === $contentType) { | if ('' === $contentType) { | |||
$contentType = $this->ContentType; | $contentType = $this->ContentType; | |||
} | } | |||
if ('' === $encoding) { | if ('' === $encoding) { | |||
$encoding = $this->Encoding; | $encoding = $this->Encoding; | |||
} | } | |||
$result .= $this->textLine('--' . $boundary); | $result .= $this->textLine('--' . $boundary); | |||
$result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSe t); | $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSe t); | |||
$result .= static::$LE; | $result .= static::$LE; | |||
// RFC1341 part 5 says 7bit is assumed if not specified | //RFC1341 part 5 says 7bit is assumed if not specified | |||
if (static::ENCODING_7BIT !== $encoding) { | if (static::ENCODING_7BIT !== $encoding) { | |||
$result .= $this->headerLine('Content-Transfer-Encoding', $encoding) ; | $result .= $this->headerLine('Content-Transfer-Encoding', $encoding) ; | |||
} | } | |||
$result .= static::$LE; | $result .= static::$LE; | |||
return $result; | return $result; | |||
} | } | |||
/** | /** | |||
* Return the end of a message boundary. | * Return the end of a message boundary. | |||
skipping to change at line 3008 | skipping to change at line 3059 | |||
$name = '', | $name = '', | |||
$encoding = self::ENCODING_BASE64, | $encoding = self::ENCODING_BASE64, | |||
$type = '', | $type = '', | |||
$disposition = 'attachment' | $disposition = 'attachment' | |||
) { | ) { | |||
try { | try { | |||
if (!static::fileIsAccessible($path)) { | if (!static::fileIsAccessible($path)) { | |||
throw new Exception($this->lang('file_access') . $path, self::ST OP_CONTINUE); | throw new Exception($this->lang('file_access') . $path, self::ST OP_CONTINUE); | |||
} | } | |||
// If a MIME type is not specified, try to work it out from the file name | //If a MIME type is not specified, try to work it out from the file name | |||
if ('' === $type) { | if ('' === $type) { | |||
$type = static::filenameToType($path); | $type = static::filenameToType($path); | |||
} | } | |||
$filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); | $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); | |||
if ('' === $name) { | if ('' === $name) { | |||
$name = $filename; | $name = $filename; | |||
} | } | |||
if (!$this->validateEncoding($encoding)) { | if (!$this->validateEncoding($encoding)) { | |||
throw new Exception($this->lang('encoding') . $encoding); | throw new Exception($this->lang('encoding') . $encoding); | |||
} | } | |||
$this->attachment[] = [ | $this->attachment[] = [ | |||
0 => $path, | 0 => $path, | |||
1 => $filename, | 1 => $filename, | |||
2 => $name, | 2 => $name, | |||
3 => $encoding, | 3 => $encoding, | |||
4 => $type, | 4 => $type, | |||
5 => false, // isStringAttachment | 5 => false, //isStringAttachment | |||
6 => $disposition, | 6 => $disposition, | |||
7 => $name, | 7 => $name, | |||
]; | ]; | |||
} catch (Exception $exc) { | } catch (Exception $exc) { | |||
$this->setError($exc->getMessage()); | $this->setError($exc->getMessage()); | |||
$this->edebug($exc->getMessage()); | $this->edebug($exc->getMessage()); | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw $exc; | throw $exc; | |||
} | } | |||
skipping to change at line 3067 | skipping to change at line 3118 | |||
* | * | |||
* @param string $disposition_type | * @param string $disposition_type | |||
* @param string $boundary | * @param string $boundary | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
protected function attachAll($disposition_type, $boundary) | protected function attachAll($disposition_type, $boundary) | |||
{ | { | |||
// Return text of body | //Return text of body | |||
$mime = []; | $mime = []; | |||
$cidUniq = []; | $cidUniq = []; | |||
$incl = []; | $incl = []; | |||
// Add all attachments | //Add all attachments | |||
foreach ($this->attachment as $attachment) { | foreach ($this->attachment as $attachment) { | |||
// Check if it is a valid disposition_filter | //Check if it is a valid disposition_filter | |||
if ($attachment[6] === $disposition_type) { | if ($attachment[6] === $disposition_type) { | |||
// Check for string attachment | //Check for string attachment | |||
$string = ''; | $string = ''; | |||
$path = ''; | $path = ''; | |||
$bString = $attachment[5]; | $bString = $attachment[5]; | |||
if ($bString) { | if ($bString) { | |||
$string = $attachment[0]; | $string = $attachment[0]; | |||
} else { | } else { | |||
$path = $attachment[0]; | $path = $attachment[0]; | |||
} | } | |||
$inclhash = hash('sha256', serialize($attachment)); | $inclhash = hash('sha256', serialize($attachment)); | |||
skipping to change at line 3117 | skipping to change at line 3168 | |||
static::quotedString($this->encodeHeader($this->secureHe ader($name))), | static::quotedString($this->encodeHeader($this->secureHe ader($name))), | |||
static::$LE | static::$LE | |||
); | ); | |||
} else { | } else { | |||
$mime[] = sprintf( | $mime[] = sprintf( | |||
'Content-Type: %s%s', | 'Content-Type: %s%s', | |||
$type, | $type, | |||
static::$LE | static::$LE | |||
); | ); | |||
} | } | |||
// RFC1341 part 5 says 7bit is assumed if not specified | //RFC1341 part 5 says 7bit is assumed if not specified | |||
if (static::ENCODING_7BIT !== $encoding) { | if (static::ENCODING_7BIT !== $encoding) { | |||
$mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encodi ng, static::$LE); | $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encodi ng, static::$LE); | |||
} | } | |||
//Only set Content-IDs on inline attachments | //Only set Content-IDs on inline attachments | |||
if ((string) $cid !== '' && $disposition === 'inline') { | if ((string) $cid !== '' && $disposition === 'inline') { | |||
$mime[] = 'Content-ID: <' . $this->encodeHeader($this->secur eHeader($cid)) . '>' . static::$LE; | $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secur eHeader($cid)) . '>' . static::$LE; | |||
} | } | |||
// Allow for bypassing the Content-Disposition header | //Allow for bypassing the Content-Disposition header | |||
if (!empty($disposition)) { | if (!empty($disposition)) { | |||
$encoded_name = $this->encodeHeader($this->secureHeader($nam e)); | $encoded_name = $this->encodeHeader($this->secureHeader($nam e)); | |||
if (!empty($encoded_name)) { | if (!empty($encoded_name)) { | |||
$mime[] = sprintf( | $mime[] = sprintf( | |||
'Content-Disposition: %s; filename=%s%s', | 'Content-Disposition: %s; filename=%s%s', | |||
$disposition, | $disposition, | |||
static::quotedString($encoded_name), | static::quotedString($encoded_name), | |||
static::$LE . static::$LE | static::$LE . static::$LE | |||
); | ); | |||
} else { | } else { | |||
$mime[] = sprintf( | $mime[] = sprintf( | |||
'Content-Disposition: %s%s', | 'Content-Disposition: %s%s', | |||
$disposition, | $disposition, | |||
static::$LE . static::$LE | static::$LE . static::$LE | |||
); | ); | |||
} | } | |||
} else { | } else { | |||
$mime[] = static::$LE; | $mime[] = static::$LE; | |||
} | } | |||
// Encode as string attachment | //Encode as string attachment | |||
if ($bString) { | if ($bString) { | |||
$mime[] = $this->encodeString($string, $encoding); | $mime[] = $this->encodeString($string, $encoding); | |||
} else { | } else { | |||
$mime[] = $this->encodeFile($path, $encoding); | $mime[] = $this->encodeFile($path, $encoding); | |||
} | } | |||
if ($this->isError()) { | if ($this->isError()) { | |||
return ''; | return ''; | |||
} | } | |||
$mime[] = static::$LE; | $mime[] = static::$LE; | |||
} | } | |||
skipping to change at line 3224 | skipping to change at line 3275 | |||
case static::ENCODING_BASE64: | case static::ENCODING_BASE64: | |||
$encoded = chunk_split( | $encoded = chunk_split( | |||
base64_encode($str), | base64_encode($str), | |||
static::STD_LINE_LENGTH, | static::STD_LINE_LENGTH, | |||
static::$LE | static::$LE | |||
); | ); | |||
break; | break; | |||
case static::ENCODING_7BIT: | case static::ENCODING_7BIT: | |||
case static::ENCODING_8BIT: | case static::ENCODING_8BIT: | |||
$encoded = static::normalizeBreaks($str); | $encoded = static::normalizeBreaks($str); | |||
// Make sure it ends with a line break | //Make sure it ends with a line break | |||
if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) { | if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) { | |||
$encoded .= static::$LE; | $encoded .= static::$LE; | |||
} | } | |||
break; | break; | |||
case static::ENCODING_BINARY: | case static::ENCODING_BINARY: | |||
$encoded = $str; | $encoded = $str; | |||
break; | break; | |||
case static::ENCODING_QUOTED_PRINTABLE: | case static::ENCODING_QUOTED_PRINTABLE: | |||
$encoded = $this->encodeQP($str); | $encoded = $this->encodeQP($str); | |||
break; | break; | |||
skipping to change at line 3262 | skipping to change at line 3313 | |||
* @param string $position What context the string will be used in | * @param string $position What context the string will be used in | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public function encodeHeader($str, $position = 'text') | public function encodeHeader($str, $position = 'text') | |||
{ | { | |||
$matchcount = 0; | $matchcount = 0; | |||
switch (strtolower($position)) { | switch (strtolower($position)) { | |||
case 'phrase': | case 'phrase': | |||
if (!preg_match('/[\200-\377]/', $str)) { | if (!preg_match('/[\200-\377]/', $str)) { | |||
// Can't use addslashes as we don't know the value of magic_ quotes_sybase | //Can't use addslashes as we don't know the value of magic_q uotes_sybase | |||
$encoded = addcslashes($str, "\0..\37\177\\\""); | $encoded = addcslashes($str, "\0..\37\177\\\""); | |||
if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'* +\/=?^_`{|}~ -]/', $str)) { | if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'* +\/=?^_`{|}~ -]/', $str)) { | |||
return $encoded; | return $encoded; | |||
} | } | |||
return "\"$encoded\""; | return "\"$encoded\""; | |||
} | } | |||
$matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); | $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); | |||
break; | break; | |||
/* @noinspection PhpMissingBreakStatementInspection */ | /* @noinspection PhpMissingBreakStatementInspection */ | |||
skipping to change at line 3288 | skipping to change at line 3339 | |||
$matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177- \377]/', $str, $matches); | $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177- \377]/', $str, $matches); | |||
break; | break; | |||
} | } | |||
if ($this->has8bitChars($str)) { | if ($this->has8bitChars($str)) { | |||
$charset = $this->CharSet; | $charset = $this->CharSet; | |||
} else { | } else { | |||
$charset = static::CHARSET_ASCII; | $charset = static::CHARSET_ASCII; | |||
} | } | |||
// Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<conte nt>?=`"). | //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<conten t>?=`"). | |||
$overhead = 8 + strlen($charset); | $overhead = 8 + strlen($charset); | |||
if ('mail' === $this->Mailer) { | if ('mail' === $this->Mailer) { | |||
$maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead; | $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead; | |||
} else { | } else { | |||
$maxlen = static::MAX_LINE_LENGTH - $overhead; | $maxlen = static::MAX_LINE_LENGTH - $overhead; | |||
} | } | |||
// Select the encoding that produces the shortest output and/or prevents corruption. | //Select the encoding that produces the shortest output and/or prevents corruption. | |||
if ($matchcount > strlen($str) / 3) { | if ($matchcount > strlen($str) / 3) { | |||
// More than 1/3 of the content needs encoding, use B-encode. | //More than 1/3 of the content needs encoding, use B-encode. | |||
$encoding = 'B'; | $encoding = 'B'; | |||
} elseif ($matchcount > 0) { | } elseif ($matchcount > 0) { | |||
// Less than 1/3 of the content needs encoding, use Q-encode. | //Less than 1/3 of the content needs encoding, use Q-encode. | |||
$encoding = 'Q'; | $encoding = 'Q'; | |||
} elseif (strlen($str) > $maxlen) { | } elseif (strlen($str) > $maxlen) { | |||
// No encoding needed, but value exceeds max line length, use Q-enco de to prevent corruption. | //No encoding needed, but value exceeds max line length, use Q-encod e to prevent corruption. | |||
$encoding = 'Q'; | $encoding = 'Q'; | |||
} else { | } else { | |||
// No reformatting needed | //No reformatting needed | |||
$encoding = false; | $encoding = false; | |||
} | } | |||
switch ($encoding) { | switch ($encoding) { | |||
case 'B': | case 'B': | |||
if ($this->hasMultiBytes($str)) { | if ($this->hasMultiBytes($str)) { | |||
// Use a custom function which correctly encodes and wraps l | //Use a custom function which correctly encodes and wraps lo | |||
ong | ng | |||
// multibyte strings without breaking lines within a charact | //multibyte strings without breaking lines within a characte | |||
er | r | |||
$encoded = $this->base64EncodeWrapMB($str, "\n"); | $encoded = $this->base64EncodeWrapMB($str, "\n"); | |||
} else { | } else { | |||
$encoded = base64_encode($str); | $encoded = base64_encode($str); | |||
$maxlen -= $maxlen % 4; | $maxlen -= $maxlen % 4; | |||
$encoded = trim(chunk_split($encoded, $maxlen, "\n")); | $encoded = trim(chunk_split($encoded, $maxlen, "\n")); | |||
} | } | |||
$encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encod ing?\\1?=", $encoded); | $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encod ing?\\1?=", $encoded); | |||
break; | break; | |||
case 'Q': | case 'Q': | |||
$encoded = $this->encodeQ($str, $position); | $encoded = $this->encodeQ($str, $position); | |||
skipping to change at line 3351 | skipping to change at line 3402 | |||
* @param string $str multi-byte text to wrap encode | * @param string $str multi-byte text to wrap encode | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
public function hasMultiBytes($str) | public function hasMultiBytes($str) | |||
{ | { | |||
if (function_exists('mb_strlen')) { | if (function_exists('mb_strlen')) { | |||
return strlen($str) > mb_strlen($str, $this->CharSet); | return strlen($str) > mb_strlen($str, $this->CharSet); | |||
} | } | |||
// Assume no multibytes (we can't handle without mbstring functions anyw ay) | //Assume no multibytes (we can't handle without mbstring functions anywa y) | |||
return false; | return false; | |||
} | } | |||
/** | /** | |||
* Does a string contain any 8-bit chars (in any charset)? | * Does a string contain any 8-bit chars (in any charset)? | |||
* | * | |||
* @param string $text | * @param string $text | |||
* | * | |||
* @return bool | * @return bool | |||
*/ | */ | |||
skipping to change at line 3389 | skipping to change at line 3440 | |||
public function base64EncodeWrapMB($str, $linebreak = null) | public function base64EncodeWrapMB($str, $linebreak = null) | |||
{ | { | |||
$start = '=?' . $this->CharSet . '?B?'; | $start = '=?' . $this->CharSet . '?B?'; | |||
$end = '?='; | $end = '?='; | |||
$encoded = ''; | $encoded = ''; | |||
if (null === $linebreak) { | if (null === $linebreak) { | |||
$linebreak = static::$LE; | $linebreak = static::$LE; | |||
} | } | |||
$mb_length = mb_strlen($str, $this->CharSet); | $mb_length = mb_strlen($str, $this->CharSet); | |||
// Each line must have length <= 75, including $start and $end | //Each line must have length <= 75, including $start and $end | |||
$length = 75 - strlen($start) - strlen($end); | $length = 75 - strlen($start) - strlen($end); | |||
// Average multi-byte ratio | //Average multi-byte ratio | |||
$ratio = $mb_length / strlen($str); | $ratio = $mb_length / strlen($str); | |||
// Base64 has a 4:3 ratio | //Base64 has a 4:3 ratio | |||
$avgLength = floor($length * $ratio * .75); | $avgLength = floor($length * $ratio * .75); | |||
$offset = 0; | $offset = 0; | |||
for ($i = 0; $i < $mb_length; $i += $offset) { | for ($i = 0; $i < $mb_length; $i += $offset) { | |||
$lookBack = 0; | $lookBack = 0; | |||
do { | do { | |||
$offset = $avgLength - $lookBack; | $offset = $avgLength - $lookBack; | |||
$chunk = mb_substr($str, $i, $offset, $this->CharSet); | $chunk = mb_substr($str, $i, $offset, $this->CharSet); | |||
$chunk = base64_encode($chunk); | $chunk = base64_encode($chunk); | |||
++$lookBack; | ++$lookBack; | |||
} while (strlen($chunk) > $length); | } while (strlen($chunk) > $length); | |||
$encoded .= $chunk . $linebreak; | $encoded .= $chunk . $linebreak; | |||
} | } | |||
// Chomp the last linefeed | //Chomp the last linefeed | |||
return substr($encoded, 0, -strlen($linebreak)); | return substr($encoded, 0, -strlen($linebreak)); | |||
} | } | |||
/** | /** | |||
* Encode a string in quoted-printable format. | * Encode a string in quoted-printable format. | |||
* According to RFC2045 section 6.7. | * According to RFC2045 section 6.7. | |||
* | * | |||
* @param string $string The text to encode | * @param string $string The text to encode | |||
* | * | |||
* @return string | * @return string | |||
skipping to change at line 3437 | skipping to change at line 3488 | |||
* | * | |||
* @see http://tools.ietf.org/html/rfc2047#section-4.2 | * @see http://tools.ietf.org/html/rfc2047#section-4.2 | |||
* | * | |||
* @param string $str the text to encode | * @param string $str the text to encode | |||
* @param string $position Where the text is going to be used, see the RFC f or what that means | * @param string $position Where the text is going to be used, see the RFC f or what that means | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public function encodeQ($str, $position = 'text') | public function encodeQ($str, $position = 'text') | |||
{ | { | |||
// There should not be any EOL in the string | //There should not be any EOL in the string | |||
$pattern = ''; | $pattern = ''; | |||
$encoded = str_replace(["\r", "\n"], '', $str); | $encoded = str_replace(["\r", "\n"], '', $str); | |||
switch (strtolower($position)) { | switch (strtolower($position)) { | |||
case 'phrase': | case 'phrase': | |||
// RFC 2047 section 5.3 | //RFC 2047 section 5.3 | |||
$pattern = '^A-Za-z0-9!*+\/ -'; | $pattern = '^A-Za-z0-9!*+\/ -'; | |||
break; | break; | |||
/* | /* | |||
* RFC 2047 section 5.2. | * RFC 2047 section 5.2. | |||
* Build $pattern without including delimiters and [] | * Build $pattern without including delimiters and [] | |||
*/ | */ | |||
/* @noinspection PhpMissingBreakStatementInspection */ | /* @noinspection PhpMissingBreakStatementInspection */ | |||
case 'comment': | case 'comment': | |||
$pattern = '\(\)"'; | $pattern = '\(\)"'; | |||
/* Intentional fall through */ | /* Intentional fall through */ | |||
case 'text': | case 'text': | |||
default: | default: | |||
// RFC 2047 section 5.1 | //RFC 2047 section 5.1 | |||
// Replace every high ascii, control, =, ? and _ characters | //Replace every high ascii, control, =, ? and _ characters | |||
$pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $ pattern; | $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $ pattern; | |||
break; | break; | |||
} | } | |||
$matches = []; | $matches = []; | |||
if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { | if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { | |||
// If the string contains an '=', make sure it's the first thing we | //If the string contains an '=', make sure it's the first thing we r | |||
replace | eplace | |||
// so as to avoid double-encoding | //so as to avoid double-encoding | |||
$eqkey = array_search('=', $matches[0], true); | $eqkey = array_search('=', $matches[0], true); | |||
if (false !== $eqkey) { | if (false !== $eqkey) { | |||
unset($matches[0][$eqkey]); | unset($matches[0][$eqkey]); | |||
array_unshift($matches[0], '='); | array_unshift($matches[0], '='); | |||
} | } | |||
foreach (array_unique($matches[0]) as $char) { | foreach (array_unique($matches[0]) as $char) { | |||
$encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); | $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); | |||
} | } | |||
} | } | |||
// Replace spaces with _ (more readable than =20) | //Replace spaces with _ (more readable than =20) | |||
// RFC 2047 section 4.2(2) | //RFC 2047 section 4.2(2) | |||
return str_replace(' ', '_', $encoded); | return str_replace(' ', '_', $encoded); | |||
} | } | |||
/** | /** | |||
* Add a string or binary attachment (non-filesystem). | * Add a string or binary attachment (non-filesystem). | |||
* This method can be used to attach ascii or binary data, | * This method can be used to attach ascii or binary data, | |||
* such as a BLOB record from a database. | * such as a BLOB record from a database. | |||
* | * | |||
* @param string $string String attachment data | * @param string $string String attachment data | |||
* @param string $filename Name of the attachment | * @param string $filename Name of the attachment | |||
skipping to change at line 3501 | skipping to change at line 3552 | |||
* @return bool True on successfully adding an attachment | * @return bool True on successfully adding an attachment | |||
*/ | */ | |||
public function addStringAttachment( | public function addStringAttachment( | |||
$string, | $string, | |||
$filename, | $filename, | |||
$encoding = self::ENCODING_BASE64, | $encoding = self::ENCODING_BASE64, | |||
$type = '', | $type = '', | |||
$disposition = 'attachment' | $disposition = 'attachment' | |||
) { | ) { | |||
try { | try { | |||
// If a MIME type is not specified, try to work it out from the file name | //If a MIME type is not specified, try to work it out from the file name | |||
if ('' === $type) { | if ('' === $type) { | |||
$type = static::filenameToType($filename); | $type = static::filenameToType($filename); | |||
} | } | |||
if (!$this->validateEncoding($encoding)) { | if (!$this->validateEncoding($encoding)) { | |||
throw new Exception($this->lang('encoding') . $encoding); | throw new Exception($this->lang('encoding') . $encoding); | |||
} | } | |||
// Append to $attachment array | //Append to $attachment array | |||
$this->attachment[] = [ | $this->attachment[] = [ | |||
0 => $string, | 0 => $string, | |||
1 => $filename, | 1 => $filename, | |||
2 => static::mb_pathinfo($filename, PATHINFO_BASENAME), | 2 => static::mb_pathinfo($filename, PATHINFO_BASENAME), | |||
3 => $encoding, | 3 => $encoding, | |||
4 => $type, | 4 => $type, | |||
5 => true, // isStringAttachment | 5 => true, //isStringAttachment | |||
6 => $disposition, | 6 => $disposition, | |||
7 => 0, | 7 => 0, | |||
]; | ]; | |||
} catch (Exception $exc) { | } catch (Exception $exc) { | |||
$this->setError($exc->getMessage()); | $this->setError($exc->getMessage()); | |||
$this->edebug($exc->getMessage()); | $this->edebug($exc->getMessage()); | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw $exc; | throw $exc; | |||
} | } | |||
skipping to change at line 3568 | skipping to change at line 3619 | |||
$name = '', | $name = '', | |||
$encoding = self::ENCODING_BASE64, | $encoding = self::ENCODING_BASE64, | |||
$type = '', | $type = '', | |||
$disposition = 'inline' | $disposition = 'inline' | |||
) { | ) { | |||
try { | try { | |||
if (!static::fileIsAccessible($path)) { | if (!static::fileIsAccessible($path)) { | |||
throw new Exception($this->lang('file_access') . $path, self::ST OP_CONTINUE); | throw new Exception($this->lang('file_access') . $path, self::ST OP_CONTINUE); | |||
} | } | |||
// If a MIME type is not specified, try to work it out from the file name | //If a MIME type is not specified, try to work it out from the file name | |||
if ('' === $type) { | if ('' === $type) { | |||
$type = static::filenameToType($path); | $type = static::filenameToType($path); | |||
} | } | |||
if (!$this->validateEncoding($encoding)) { | if (!$this->validateEncoding($encoding)) { | |||
throw new Exception($this->lang('encoding') . $encoding); | throw new Exception($this->lang('encoding') . $encoding); | |||
} | } | |||
$filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); | $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); | |||
if ('' === $name) { | if ('' === $name) { | |||
$name = $filename; | $name = $filename; | |||
} | } | |||
// Append to $attachment array | //Append to $attachment array | |||
$this->attachment[] = [ | $this->attachment[] = [ | |||
0 => $path, | 0 => $path, | |||
1 => $filename, | 1 => $filename, | |||
2 => $name, | 2 => $name, | |||
3 => $encoding, | 3 => $encoding, | |||
4 => $type, | 4 => $type, | |||
5 => false, // isStringAttachment | 5 => false, //isStringAttachment | |||
6 => $disposition, | 6 => $disposition, | |||
7 => $cid, | 7 => $cid, | |||
]; | ]; | |||
} catch (Exception $exc) { | } catch (Exception $exc) { | |||
$this->setError($exc->getMessage()); | $this->setError($exc->getMessage()); | |||
$this->edebug($exc->getMessage()); | $this->edebug($exc->getMessage()); | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw $exc; | throw $exc; | |||
} | } | |||
skipping to change at line 3634 | skipping to change at line 3685 | |||
*/ | */ | |||
public function addStringEmbeddedImage( | public function addStringEmbeddedImage( | |||
$string, | $string, | |||
$cid, | $cid, | |||
$name = '', | $name = '', | |||
$encoding = self::ENCODING_BASE64, | $encoding = self::ENCODING_BASE64, | |||
$type = '', | $type = '', | |||
$disposition = 'inline' | $disposition = 'inline' | |||
) { | ) { | |||
try { | try { | |||
// If a MIME type is not specified, try to work it out from the name | //If a MIME type is not specified, try to work it out from the name | |||
if ('' === $type && !empty($name)) { | if ('' === $type && !empty($name)) { | |||
$type = static::filenameToType($name); | $type = static::filenameToType($name); | |||
} | } | |||
if (!$this->validateEncoding($encoding)) { | if (!$this->validateEncoding($encoding)) { | |||
throw new Exception($this->lang('encoding') . $encoding); | throw new Exception($this->lang('encoding') . $encoding); | |||
} | } | |||
// Append to $attachment array | //Append to $attachment array | |||
$this->attachment[] = [ | $this->attachment[] = [ | |||
0 => $string, | 0 => $string, | |||
1 => $name, | 1 => $name, | |||
2 => $name, | 2 => $name, | |||
3 => $encoding, | 3 => $encoding, | |||
4 => $type, | 4 => $type, | |||
5 => true, // isStringAttachment | 5 => true, //isStringAttachment | |||
6 => $disposition, | 6 => $disposition, | |||
7 => $cid, | 7 => $cid, | |||
]; | ]; | |||
} catch (Exception $exc) { | } catch (Exception $exc) { | |||
$this->setError($exc->getMessage()); | $this->setError($exc->getMessage()); | |||
$this->edebug($exc->getMessage()); | $this->edebug($exc->getMessage()); | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw $exc; | throw $exc; | |||
} | } | |||
skipping to change at line 3870 | skipping to change at line 3921 | |||
$this->ErrorInfo = $msg; | $this->ErrorInfo = $msg; | |||
} | } | |||
/** | /** | |||
* Return an RFC 822 formatted date. | * Return an RFC 822 formatted date. | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public static function rfcDate() | public static function rfcDate() | |||
{ | { | |||
// Set the time zone to whatever the default is to avoid 500 errors | //Set the time zone to whatever the default is to avoid 500 errors | |||
// Will default to UTC if it's not set properly in php.ini | //Will default to UTC if it's not set properly in php.ini | |||
date_default_timezone_set(@date_default_timezone_get()); | date_default_timezone_set(@date_default_timezone_get()); | |||
return date('D, j M Y H:i:s O'); | return date('D, j M Y H:i:s O'); | |||
} | } | |||
/** | /** | |||
* Get the server hostname. | * Get the server hostname. | |||
* Returns 'localhost.localdomain' if unknown. | * Returns 'localhost.localdomain' if unknown. | |||
* | * | |||
* @return string | * @return string | |||
skipping to change at line 3949 | skipping to change at line 4000 | |||
/** | /** | |||
* Get an error message in the current language. | * Get an error message in the current language. | |||
* | * | |||
* @param string $key | * @param string $key | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
protected function lang($key) | protected function lang($key) | |||
{ | { | |||
if (count($this->language) < 1) { | if (count($this->language) < 1) { | |||
$this->setLanguage(); // set the default language | $this->setLanguage(); //Set the default language | |||
} | } | |||
if (array_key_exists($key, $this->language)) { | if (array_key_exists($key, $this->language)) { | |||
if ('smtp_connect_failed' === $key) { | if ('smtp_connect_failed' === $key) { | |||
//Include a link to troubleshooting docs on SMTP connection fail | //Include a link to troubleshooting docs on SMTP connection fail | |||
ure | ure. | |||
//this is by far the biggest cause of support questions | //This is by far the biggest cause of support questions | |||
//but it's usually not PHPMailer's fault. | //but it's usually not PHPMailer's fault. | |||
return $this->language[$key] . ' https://github.com/PHPMailer/PH PMailer/wiki/Troubleshooting'; | return $this->language[$key] . ' https://github.com/PHPMailer/PH PMailer/wiki/Troubleshooting'; | |||
} | } | |||
return $this->language[$key]; | return $this->language[$key]; | |||
} | } | |||
//Return the key as a fallback | //Return the key as a fallback | |||
return $key; | return $key; | |||
} | } | |||
skipping to change at line 3990 | skipping to change at line 4041 | |||
* both header name and value (name:value). | * both header name and value (name:value). | |||
* | * | |||
* @param string $name Custom header name | * @param string $name Custom header name | |||
* @param string|null $value Header value | * @param string|null $value Header value | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
*/ | */ | |||
public function addCustomHeader($name, $value = null) | public function addCustomHeader($name, $value = null) | |||
{ | { | |||
if (null === $value && strpos($name, ':') !== false) { | if (null === $value && strpos($name, ':') !== false) { | |||
// Value passed in as name:value | //Value passed in as name:value | |||
list($name, $value) = explode(':', $name, 2); | list($name, $value) = explode(':', $name, 2); | |||
} | } | |||
$name = trim($name); | $name = trim($name); | |||
$value = trim($value); | $value = trim($value); | |||
//Ensure name is not empty, and that neither name nor value contain line breaks | //Ensure name is not empty, and that neither name nor value contain line breaks | |||
if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { | if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { | |||
if ($this->exceptions) { | if ($this->exceptions) { | |||
throw new Exception('Invalid header name or value'); | throw new Exception('Invalid header name or value'); | |||
} | } | |||
skipping to change at line 4044 | skipping to change at line 4095 | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @see PHPMailer::html2text() | * @see PHPMailer::html2text() | |||
*/ | */ | |||
public function msgHTML($message, $basedir = '', $advanced = false) | public function msgHTML($message, $basedir = '', $advanced = false) | |||
{ | { | |||
preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $i mages); | preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $i mages); | |||
if (array_key_exists(2, $images)) { | if (array_key_exists(2, $images)) { | |||
if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) { | if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) { | |||
// Ensure $basedir has a trailing / | //Ensure $basedir has a trailing / | |||
$basedir .= '/'; | $basedir .= '/'; | |||
} | } | |||
foreach ($images[2] as $imgindex => $url) { | foreach ($images[2] as $imgindex => $url) { | |||
// Convert data URIs into embedded images | //Convert data URIs into embedded images | |||
//e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA ABAAEAAAICTAEAOw==" | //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA ABAAEAAAICTAEAOw==" | |||
$match = []; | $match = []; | |||
if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+) #', $url, $match)) { | if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+) #', $url, $match)) { | |||
if (count($match) === 4 && static::ENCODING_BASE64 === $matc h[2]) { | if (count($match) === 4 && static::ENCODING_BASE64 === $matc h[2]) { | |||
$data = base64_decode($match[3]); | $data = base64_decode($match[3]); | |||
} elseif ('' === $match[2]) { | } elseif ('' === $match[2]) { | |||
$data = rawurldecode($match[3]); | $data = rawurldecode($match[3]); | |||
} else { | } else { | |||
//Not recognised so leave it alone | //Not recognised so leave it alone | |||
continue; | continue; | |||
} | } | |||
//Hash the decoded data, not the URL, so that the same data- URI image used in multiple places | //Hash the decoded data, not the URL, so that the same data- URI image used in multiple places | |||
//will only be embedded once, even if it used a different en coding | //will only be embedded once, even if it used a different en coding | |||
$cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0' ; // RFC2392 S 2 | $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0' ; //RFC2392 S 2 | |||
if (!$this->cidExists($cid)) { | if (!$this->cidExists($cid)) { | |||
$this->addStringEmbeddedImage( | $this->addStringEmbeddedImage( | |||
$data, | $data, | |||
$cid, | $cid, | |||
'embed' . $imgindex, | 'embed' . $imgindex, | |||
static::ENCODING_BASE64, | static::ENCODING_BASE64, | |||
$match[1] | $match[1] | |||
); | ); | |||
} | } | |||
$message = str_replace( | $message = str_replace( | |||
$images[0][$imgindex], | $images[0][$imgindex], | |||
$images[1][$imgindex] . '="cid:' . $cid . '"', | $images[1][$imgindex] . '="cid:' . $cid . '"', | |||
$message | $message | |||
); | ); | |||
continue; | continue; | |||
} | } | |||
if ( | if ( | |||
// Only process relative URLs if a basedir is provided (i.e. no absolute local paths) | //Only process relative URLs if a basedir is provided (i.e. no absolute local paths) | |||
!empty($basedir) | !empty($basedir) | |||
// Ignore URLs containing parent dir traversal (..) | //Ignore URLs containing parent dir traversal (..) | |||
&& (strpos($url, '..') === false) | && (strpos($url, '..') === false) | |||
// Do not change urls that are already inline images | //Do not change urls that are already inline images | |||
&& 0 !== strpos($url, 'cid:') | && 0 !== strpos($url, 'cid:') | |||
// Do not change absolute URLs, including anonymous protocol | //Do not change absolute URLs, including anonymous protocol | |||
&& !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) | && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) | |||
) { | ) { | |||
$filename = static::mb_pathinfo($url, PATHINFO_BASENAME); | $filename = static::mb_pathinfo($url, PATHINFO_BASENAME); | |||
$directory = dirname($url); | $directory = dirname($url); | |||
if ('.' === $directory) { | if ('.' === $directory) { | |||
$directory = ''; | $directory = ''; | |||
} | } | |||
// RFC2392 S 2 | //RFC2392 S 2 | |||
$cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0'; | $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0'; | |||
if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) { | if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) { | |||
$basedir .= '/'; | $basedir .= '/'; | |||
} | } | |||
if (strlen($directory) > 1 && '/' !== substr($directory, -1) ) { | if (strlen($directory) > 1 && '/' !== substr($directory, -1) ) { | |||
$directory .= '/'; | $directory .= '/'; | |||
} | } | |||
if ( | if ( | |||
$this->addEmbeddedImage( | $this->addEmbeddedImage( | |||
$basedir . $directory . $filename, | $basedir . $directory . $filename, | |||
skipping to change at line 4122 | skipping to change at line 4173 | |||
$message = preg_replace( | $message = preg_replace( | |||
'/' . $images[1][$imgindex] . '=["\']' . preg_quote( $url, '/') . '["\']/Ui', | '/' . $images[1][$imgindex] . '=["\']' . preg_quote( $url, '/') . '["\']/Ui', | |||
$images[1][$imgindex] . '="cid:' . $cid . '"', | $images[1][$imgindex] . '="cid:' . $cid . '"', | |||
$message | $message | |||
); | ); | |||
} | } | |||
} | } | |||
} | } | |||
} | } | |||
$this->isHTML(); | $this->isHTML(); | |||
// Convert all message body line breaks to LE, makes quoted-printable en coding work much better | //Convert all message body line breaks to LE, makes quoted-printable enc oding work much better | |||
$this->Body = static::normalizeBreaks($message); | $this->Body = static::normalizeBreaks($message); | |||
$this->AltBody = static::normalizeBreaks($this->html2text($message, $adv anced)); | $this->AltBody = static::normalizeBreaks($this->html2text($message, $adv anced)); | |||
if (!$this->alternativeExists()) { | if (!$this->alternativeExists()) { | |||
$this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.' | $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.' | |||
. static::$LE; | . static::$LE; | |||
} | } | |||
return $this->Body; | return $this->Body; | |||
} | } | |||
/** | /** | |||
* Convert an HTML string into plain text. | * Convert an HTML string into plain text. | |||
* This is used by msgHTML(). | * This is used by msgHTML(). | |||
* Note - older versions of this function used a bundled advanced converter | * Note - older versions of this function used a bundled advanced converter | |||
* which was removed for license reasons in #232. | * which was removed for license reasons in #232. | |||
* Example usage: | * Example usage: | |||
* | * | |||
* ```php | * ```php | |||
* // Use default conversion | * //Use default conversion | |||
* $plain = $mail->html2text($html); | * $plain = $mail->html2text($html); | |||
* // Use your own custom converter | * //Use your own custom converter | |||
* $plain = $mail->html2text($html, function($html) { | * $plain = $mail->html2text($html, function($html) { | |||
* $converter = new MyHtml2text($html); | * $converter = new MyHtml2text($html); | |||
* return $converter->get_text(); | * return $converter->get_text(); | |||
* }); | * }); | |||
* ``` | * ``` | |||
* | * | |||
* @param string $html The HTML text to convert | * @param string $html The HTML text to convert | |||
* @param bool|callable $advanced Any boolean value to use the internal conv erter, | * @param bool|callable $advanced Any boolean value to use the internal conv erter, | |||
* or provide your own callable for custom co nversion | * or provide your own callable for custom co nversion | |||
* | * | |||
skipping to change at line 4310 | skipping to change at line 4361 | |||
/** | /** | |||
* Map a file name to a MIME type. | * Map a file name to a MIME type. | |||
* Defaults to 'application/octet-stream', i.e.. arbitrary binary data. | * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. | |||
* | * | |||
* @param string $filename A file name or full path, does not need to exist as a file | * @param string $filename A file name or full path, does not need to exist as a file | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public static function filenameToType($filename) | public static function filenameToType($filename) | |||
{ | { | |||
// In case the path is a URL, strip any query string before getting exte nsion | //In case the path is a URL, strip any query string before getting exten sion | |||
$qpos = strpos($filename, '?'); | $qpos = strpos($filename, '?'); | |||
if (false !== $qpos) { | if (false !== $qpos) { | |||
$filename = substr($filename, 0, $qpos); | $filename = substr($filename, 0, $qpos); | |||
} | } | |||
$ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION); | $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION); | |||
return static::_mime_types($ext); | return static::_mime_types($ext); | |||
} | } | |||
/** | /** | |||
skipping to change at line 4421 | skipping to change at line 4472 | |||
* @param string $text | * @param string $text | |||
* @param string $breaktype What kind of line break to use; defaults to stat ic::$LE | * @param string $breaktype What kind of line break to use; defaults to stat ic::$LE | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public static function normalizeBreaks($text, $breaktype = null) | public static function normalizeBreaks($text, $breaktype = null) | |||
{ | { | |||
if (null === $breaktype) { | if (null === $breaktype) { | |||
$breaktype = static::$LE; | $breaktype = static::$LE; | |||
} | } | |||
// Normalise to \n | //Normalise to \n | |||
$text = str_replace([self::CRLF, "\r"], "\n", $text); | $text = str_replace([self::CRLF, "\r"], "\n", $text); | |||
// Now convert LE as needed | //Now convert LE as needed | |||
if ("\n" !== $breaktype) { | if ("\n" !== $breaktype) { | |||
$text = str_replace("\n", $breaktype, $text); | $text = str_replace("\n", $breaktype, $text); | |||
} | } | |||
return $text; | return $text; | |||
} | } | |||
/** | /** | |||
* Remove trailing breaks from a string. | * Remove trailing breaks from a string. | |||
* | * | |||
skipping to change at line 4529 | skipping to change at line 4580 | |||
} | } | |||
$privKeyStr = !empty($this->DKIM_private_string) ? | $privKeyStr = !empty($this->DKIM_private_string) ? | |||
$this->DKIM_private_string : | $this->DKIM_private_string : | |||
file_get_contents($this->DKIM_private); | file_get_contents($this->DKIM_private); | |||
if ('' !== $this->DKIM_passphrase) { | if ('' !== $this->DKIM_passphrase) { | |||
$privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphr ase); | $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphr ase); | |||
} else { | } else { | |||
$privKey = openssl_pkey_get_private($privKeyStr); | $privKey = openssl_pkey_get_private($privKeyStr); | |||
} | } | |||
if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryp tion')) { | if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryp tion')) { | |||
if (PHP_MAJOR_VERSION < 8) { | if (\PHP_MAJOR_VERSION < 8) { | |||
openssl_pkey_free($privKey); | openssl_pkey_free($privKey); | |||
} | } | |||
return base64_encode($signature); | return base64_encode($signature); | |||
} | } | |||
if (PHP_MAJOR_VERSION < 8) { | if (\PHP_MAJOR_VERSION < 8) { | |||
openssl_pkey_free($privKey); | openssl_pkey_free($privKey); | |||
} | } | |||
return ''; | return ''; | |||
} | } | |||
/** | /** | |||
* Generate a DKIM canonicalization header. | * Generate a DKIM canonicalization header. | |||
* Uses the 'relaxed' algorithm from RFC6376 section 3.4.2. | * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2. | |||
* Canonicalized headers should *always* use CRLF, regardless of mailer sett ing. | * Canonicalized headers should *always* use CRLF, regardless of mailer sett ing. | |||
skipping to change at line 4602 | skipping to change at line 4653 | |||
* | * | |||
* @param string $body Message Body | * @param string $body Message Body | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public function DKIM_BodyC($body) | public function DKIM_BodyC($body) | |||
{ | { | |||
if (empty($body)) { | if (empty($body)) { | |||
return self::CRLF; | return self::CRLF; | |||
} | } | |||
// Normalize line endings to CRLF | //Normalize line endings to CRLF | |||
$body = static::normalizeBreaks($body, self::CRLF); | $body = static::normalizeBreaks($body, self::CRLF); | |||
//Reduce multiple trailing line breaks to a single one | //Reduce multiple trailing line breaks to a single one | |||
return static::stripTrailingWSP($body) . self::CRLF; | return static::stripTrailingWSP($body) . self::CRLF; | |||
} | } | |||
/** | /** | |||
* Create the DKIM header and body in a new message header. | * Create the DKIM header and body in a new message header. | |||
* | * | |||
* @param string $headers_line Header lines | * @param string $headers_line Header lines | |||
* @param string $subject Subject | * @param string $subject Subject | |||
* @param string $body Body | * @param string $body Body | |||
* | * | |||
* @throws Exception | * @throws Exception | |||
* | * | |||
* @return string | * @return string | |||
*/ | */ | |||
public function DKIM_Add($headers_line, $subject, $body) | public function DKIM_Add($headers_line, $subject, $body) | |||
{ | { | |||
$DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms | $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms | |||
$DKIMcanonicalization = 'relaxed/simple'; // Canonicalization methods of | $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of | |||
header & body | header & body | |||
$DKIMquery = 'dns/txt'; // Query method | $DKIMquery = 'dns/txt'; //Query method | |||
$DKIMtime = time(); | $DKIMtime = time(); | |||
//Always sign these headers without being asked | //Always sign these headers without being asked | |||
//Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4. 1 | //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4. 1 | |||
$autoSignHeaders = [ | $autoSignHeaders = [ | |||
'from', | 'from', | |||
'to', | 'to', | |||
'cc', | 'cc', | |||
'date', | 'date', | |||
'subject', | 'subject', | |||
'reply-to', | 'reply-to', | |||
skipping to change at line 4725 | skipping to change at line 4776 | |||
} else { | } else { | |||
$copiedHeaderFields .= $copiedHeader; | $copiedHeaderFields .= $copiedHeader; | |||
} | } | |||
$first = false; | $first = false; | |||
} | } | |||
$copiedHeaderFields .= ';' . static::$LE; | $copiedHeaderFields .= ';' . static::$LE; | |||
} | } | |||
$headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$ LE; | $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$ LE; | |||
$headerValues = implode(static::$LE, $headersToSign); | $headerValues = implode(static::$LE, $headersToSign); | |||
$body = $this->DKIM_BodyC($body); | $body = $this->DKIM_BodyC($body); | |||
$DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 o | //Base64 of packed binary SHA-256 hash of body | |||
f packed binary SHA-256 hash of body | $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); | |||
$ident = ''; | $ident = ''; | |||
if ('' !== $this->DKIM_identity) { | if ('' !== $this->DKIM_identity) { | |||
$ident = ' i=' . $this->DKIM_identity . ';' . static::$LE; | $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE; | |||
} | } | |||
//The DKIM-Signature header is included in the signature *except for* th e value of the `b` tag | //The DKIM-Signature header is included in the signature *except for* th e value of the `b` tag | |||
//which is appended after calculating the signature | //which is appended after calculating the signature | |||
//https://tools.ietf.org/html/rfc6376#section-3.5 | //https://tools.ietf.org/html/rfc6376#section-3.5 | |||
$dkimSignatureHeader = 'DKIM-Signature: v=1;' . | $dkimSignatureHeader = 'DKIM-Signature: v=1;' . | |||
' d=' . $this->DKIM_domain . ';' . | ' d=' . $this->DKIM_domain . ';' . | |||
' s=' . $this->DKIM_selector . ';' . static::$LE . | ' s=' . $this->DKIM_selector . ';' . static::$LE . | |||
End of changes. 142 change blocks. | ||||
183 lines changed or deleted | 242 lines changed or added |