Skip to content

Commit

Permalink
refs #40441, fixes email addr not properly verify before sending
Browse files Browse the repository at this point in the history
  • Loading branch information
jimyhuang committed Apr 30, 2024
1 parent 4cae315 commit 3ea70e3
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 35 deletions.
10 changes: 5 additions & 5 deletions CRM/Mailing/BAO/Mailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@ protected function getVerpAndUrlsAndHeaders($job_id, $event_queue_id, $hash, $em
$unsubscribeUrl = str_replace(array('&', 'http://'), array('&', 'https://'), $urls['unsubscribeUrl']);
$headers = array(
'List-Unsubscribe' => '<'.$unsubscribeUrl.'>'.' ,'."<mailto:{$verp['unsubscribe']}>",
'From' => "\"{$this->from_name}\" <{$this->from_email}>",
'From' => CRM_Utils_Mail::formatRFC822Email($this->from_name, $this->from_email),
'Sender' => $verp['reply'],
'Return-Path' => $verp['bounce'],
'Subject' => $this->subject,
Expand Down Expand Up @@ -1255,12 +1255,12 @@ public function &compose($job_id, $event_queue_id, $hash, $contactId,
$isForward
);
//set from email who is forwarding it and not original one.
if ($fromEmail) {
if ($fromEmail && CRM_Utils_Rule::email($fromEmail)) {
unset($headers['From']);
$headers['From'] = "<{$fromEmail}>";
$headers['From'] = CRM_Utils_Mail::formatRFC822Email('', $fromEmail);
}

if ($replyToEmail && ($fromEmail != $replyToEmail)) {
if ($replyToEmail && ($fromEmail != $replyToEmail) && CRM_Utils_Mail::checkRFC822Email($fromEmail)) {
$headers['Reply-To'] = "{$replyToEmail}";
}

Expand Down Expand Up @@ -1431,7 +1431,7 @@ public function &compose($job_id, $event_queue_id, $hash, $contactId,
}
}

$headers['To'] = "{$mailParams['toName']} <{$mailParams['toEmail']}>";
$headers['To'] = CRM_Utils_Mail::formatRFC822Email($mailParams['toName'], $mailParams['toEmail']);
$headers['Precedence'] = 'bulk';
// Will test in the mail processor if the X-VERP is set in the bounced email.
// (As an option to replace real VERP for those that can't set it up)
Expand Down
10 changes: 5 additions & 5 deletions CRM/Mailing/BAO/Transactional.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public static function send(&$params, $callback = NULL) {

if (empty($params['from'])) {
$defaultNameEmail = CRM_Core_BAO_Domain::getNameAndEmail( );
$params['from'] = "{$defaultNameEmail[0]} <{$defaultNameEmail[1]}>";
$params['from'] = CRM_Utils_Mail::formatRFC822Email($defaultNameEmail[0], $defaultNameEmail[1]);
}

$tmail = new CRM_Mailing_BAO_Transactional($params);
Expand Down Expand Up @@ -405,12 +405,12 @@ public function &compose($job_id, $event_queue_id, $hash, $contactId,

list($verp, $urls, $headers) = $this->getVerpAndUrlsAndHeaders($job_id, $event_queue_id, $hash, $email, $isForward);
//set from email who is forwarding it and not original one.
if ($fromEmail) {
if ($fromEmail && CRM_Utils_Rule::email($fromEmail)) {
unset($headers['From']);
$headers['From'] = "<{$fromEmail}>";
$headers['From'] = CRM_Utils_Mail::formatRFC822Email('', $fromEmail);
}

if ($replyToEmail && ($fromEmail != $replyToEmail)) {
if ($replyToEmail && ($fromEmail != $replyToEmail) && CRM_Utils_Mail::checkRFC822Email($fromEmail)) {
$headers['Reply-To'] = "{$replyToEmail}";
}

Expand Down Expand Up @@ -588,7 +588,7 @@ public function &compose($job_id, $event_queue_id, $hash, $contactId,
}
}

$headers['To'] = "{$mailParams['toName']} <{$mailParams['toEmail']}>";
$headers['To'] = CRM_Utils_Mail::formatRFC822Email($mailParams['toName'], $mailParams['toEmail']);
// Will test in the mail processor if the X-VERP is set in the bounced email.
// (As an option to replace real VERP for those that can't set it up)
$headers['X-CiviMail-Bounce'] = $verp['bounce'];
Expand Down
184 changes: 160 additions & 24 deletions CRM/Utils/Mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class CRM_Utils_Mail {

const DMARC_MAIL_PROVIDERS = 'yahoo.com|gmail.com|msn.com|outlook.com|hotmail.com';
const DKIM_EXTERNAL_VERIFIED_FILE = 'verified_external_dkim.config';
const RFC_2822_SPECIAL_CHARS = '()<>[]:;@\,."';

/**
* Wrapper function to send mail in CiviCRM. Hooks are called from this function.
Expand Down Expand Up @@ -107,10 +108,18 @@ static function send(&$params, $callback = NULL) {
}

$headers = array();
$headers['From'] = $params['from'];
$headers['To'] = "{$params['toName']} <{$params['toEmail']}>";
$headers['Cc'] = CRM_Utils_Array::value('cc', $params);
$headers['Bcc'] = CRM_Utils_Array::value('bcc', $params);
if (self::checkRFC822Email($params['from'])) {
$headers['From'] = $params['from'];
}
else {
$fromName = self::pluckNameFromHeader($params['from']);
$fromEmail = self::pluckEmailFromHeader($params['from']);
$headers['From'] = self::formatRFC822Email($fromName, $fromEmail);
}
$headers['To'] = self::formatRFC822Email($params['toName'], $params['toEmail']);
// TODO: check cc / bcc have correct format
$headers['Cc'] = !empty($params['cc']) ? self::checkEmails(CRM_Utils_Array::value('cc', $params)) : '';
$headers['Bcc'] = !empty($params['bcc']) ? self::checkEmails(CRM_Utils_Array::value('bcc', $params)) : '';
$headers['Subject'] = CRM_Utils_Array::value('subject', $params);
$headers['Content-Type'] = $htmlMessage ? 'multipart/mixed; charset=utf-8' : 'text/plain; charset=utf-8';
$headers['Content-Disposition'] = 'inline';
Expand Down Expand Up @@ -383,38 +392,96 @@ static function &setMimeParams(&$message, $params = NULL) {
return $message->get($params);
}

static function formatRFC822Email($name, $email, $useQuote = FALSE) {
$result = NULL;
/**
* Check and remove incorrect emails
*
* @param string|array $mails
* @return string
*/
public static function checkEmails($mails) {
$mailArray = array();
if (is_string($mails)) {
preg_match_all('/(?:"[^"]*"|[^,])+/u', $mails, $matches);
if (!empty($matches[0])) {
$mailArray = $matches[0];
}
}
elseif (is_array($mails)) {
$mailArray = $mails;
}
$checked = array();
foreach($mailArray as $address) {
$address = trim($address);
if (strstr($address, '<') && strstr($address, '>')) {
$name = self::pluckNameFromHeader($address);
$email = self::pluckEmailFromHeader($address);
$formatted = self::formatRFC822Email($name, $email);
if (!empty($formatted)) {
$checked[] = $formatted;
}
}
else {
if (CRM_Utils_Rule::email($address)) {
$checked[] = '<'.$address.'>';
}
}
}
if (!empty($checked)) {
return implode(', ', $checked);
}
return '';
}

$name = trim($name);
public static function checkRFC822Email($address) {
$address = trim($address);
if (strstr($address, '<') && strstr($address, '>')) {
$name = self::pluckNameFromHeader($address);
$email = self::pluckEmailFromHeader($address);
}
else {
$name = '';
$email = trim($address);
}
if (!empty($name) && preg_match('/[' . preg_quote(self::RFC_2822_SPECIAL_CHARS) . ']/u', $name)) {
// check the name already quoted
if (!preg_match('/^"[^"]+"\s*<[^>]+@[^>]+>/i', $address)) {
return FALSE;
}
}
if (!CRM_Utils_Rule::email($email)) {
return FALSE;
}
return TRUE;
}

static function formatRFC822Email($name, $email, $useQuote = TRUE) {
$result = '';
$email = CRM_Utils_Type::escape($email, 'Email', FALSE);
if (empty($email)) {
return '';
}

// strip out double quotes if present at the beginning AND end
if (substr($name, 0, 1) == '"' &&
substr($name, -1, 1) == '"'
) {
if (substr($name, 0, 1) == '"' && substr($name, -1, 1) == '"') {
$name = substr($name, 1, -1);
}

if (!empty($name)) {
// escape the special characters
$name = str_replace(array('<', '"', '>'),
array('\<', '\"', '\>'),
$name
);
if (strpos($name, ',') !== FALSE ||
$useQuote
) {
// quote the string if it has a comma
$name = '"' . $name . '"';
$name = self::sanitizeName($name);
if (!empty($name)) {
if (preg_match('/[' . preg_quote(self::RFC_2822_SPECIAL_CHARS) . ']/u', $name)) {
$useQuote = TRUE;
}
if ($useQuote) {
$name = '"' . $name . '"';
}
$result = $name.' ';
}

$result = "$name ";
}

$result .= "<{$email}>";
return $result;
}


static function checkMailProviders($email) {
$mailProviders = str_replace('.', '\.', self::DMARC_MAIL_PROVIDERS);
if (preg_match('/'.$mailProviders.'/i', $email)) {
Expand Down Expand Up @@ -653,5 +720,74 @@ public static function validDKIMDomainList($externalOnly = FALSE, $saveFile = NU
}
return array();
}

/**
* Sanitize Email for compatible with RFC822 / RFC2822
*
* @param string $email
* @return string return empty string when false
*/
public static function sanitizeEmail($email) {
$sanitized = '';
$email = trim($email);
$filtered = filter_var($email, FILTER_SANITIZE_EMAIL);

// still not ok, try sanitize harder
if (!CRM_Utils_Rule::email($filtered)) {
list($local, $domain) = explode('@', $filtered, 2);
$local = preg_replace('/[^a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.-]/', '', $local);
$local = strtolower($local);

// no any name part left, definitely error
if ($local === '') {
return $sanitized;
}

$domain = trim($domain, " \t\n\r\0\x0B.");
if ($domain === '') {
return $sanitized;
}
if (!strstr($domain, '.')) {
return $sanitized;
}
$subs = explode('.', $domain);
$sanitizedDomain = array();
foreach($subs as $sub) {
$sub = trim($sub, " \t\n\r\0\x0B-" );
$sub = preg_replace('/[^a-z0-9-]+/i', '', $sub);
if ($sub !== '') {
$sanitizedDomain[] = strtolower($sub);
}
}
if (count($sanitizedDomain) < 2) {
return $sanitized;
}
$domain = implode('.', $sanitizedDomain);

$filtered = $local.'@'.$domain;
if (CRM_Utils_Rule::email($filtered)) {
$sanitized = $filtered;
}
}
else {
$sanitized = $filtered;
}

return $sanitized;
}

/**
* Sanitize Email Name to escape quote
*
* @param string $name
* @return string
*/
public static function sanitizeName($name) {
$string = trim($name, '"');
if (strstr($string, '"')) {
$string = str_replace('"', '\\"', $string);
}
return $string;
}
}

32 changes: 31 additions & 1 deletion CRM/Utils/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ static function typeToString($type) {
}

/**
* Verify that a variable is of a given type
* Verify that a variable is of a given type and apply a bit of processing
*
* @param mixed $data The variable
* @param string $type The type
Expand Down Expand Up @@ -225,6 +225,18 @@ public static function escape($data, $type, $abort = TRUE) {
}
break;

case 'Email':
if (CRM_Utils_Rule::email($data)) {
return $data;
}
else {
$email = CRM_Utils_Mail::sanitizeEmail($data);
if (!empty($email)) {
return $email;
}
}
break;

default:
CRM_Core_Error::fatal("Cannot recognize $type for $data");
break;
Expand Down Expand Up @@ -343,6 +355,24 @@ public static function validate($data, $type, $abort = TRUE, $name = 'One of par
}
break;

case 'DirectoryName':
if (CRM_Utils_Rule::directoryName($data)) {
return $data;
}
break;

case 'FileName':
if (CRM_Utils_Rule::fileName($data)) {
return $data;
}
break;

case 'Email':
if (CRM_Utils_Rule::email($data)) {
return $data;
}
break;

default:
CRM_Core_Error::fatal("Cannot recognize $type for data");
break;
Expand Down

0 comments on commit 3ea70e3

Please sign in to comment.