diff --git a/includes/mail.inc b/includes/mail.inc index 0e5c17804c..3b9ee708a0 100644 --- a/includes/mail.inc +++ b/includes/mail.inc @@ -12,6 +12,12 @@ */ define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) ? "\r\n" : "\n"); + +/** + * Special characters, defined in RFC_2822. + */ +define('MAIL_RFC_2822_SPECIALS', '()<>[]:;@\,."'); + /** * Composes and optionally sends an e-mail message. * @@ -148,8 +154,13 @@ function drupal_mail($module, $key, $to, $language, $params = array(), $from = N // Return-Path headers should have a domain authorized to use the originating // SMTP server. $headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $default_from; + + if (variable_get('mail_display_name_site_name', FALSE)) { + $display_name = variable_get('site_name', 'Drupal'); + $headers['From'] = drupal_mail_format_display_name($display_name) . ' <' . $default_from . '>'; + } } - if ($from) { + if ($from && $from != $default_from) { $headers['From'] = $from; } $message['headers'] = $headers; @@ -557,6 +568,55 @@ function drupal_html_to_text($string, $allowed_tags = NULL) { return $output . $footnotes; } +/** + * Return a RFC-2822 compliant "display-name" component. + * + * The "display-name" component is used in mail header "Originator" fields + * (From, Sender, Reply-to) to give a human-friendly description of the + * address, i.e. From: My Display Name . RFC-822 and + * RFC-2822 define its syntax and rules. This method gets as input a string + * to be used as "display-name" and formats it to be RFC compliant. + * + * @param string $string + * A string to be used as "display-name". + * + * @return string + * A RFC compliant version of the string, ready to be used as + * "display-name" in mail originator header fields. + */ +function drupal_mail_format_display_name($string) { + // Make sure we don't process html-encoded characters. They may create + // unneeded trouble if left encoded, besides they will be correctly + // processed if decoded. + $string = decode_entities($string); + + // If string contains non-ASCII characters it must be (short) encoded + // according to RFC-2047. The output of a "B" (Base64) encoded-word is + // always safe to be used as display-name. + $safe_display_name = mime_header_encode($string, TRUE); + + // Encoded-words are always safe to be used as display-name because don't + // contain any RFC 2822 "specials" characters. However + // mimeHeaderEncode() encodes a string only if it contains any + // non-ASCII characters, and leaves its value untouched (un-encoded) if + // ASCII only. For this reason in order to produce a valid display-name we + // still need to make sure there are no "specials" characters left. + if (preg_match('/[' . preg_quote(MAIL_RFC_2822_SPECIALS) . ']/', $safe_display_name)) { + + // If string is already quoted, it may or may not be escaped properly, so + // don't trust it and reset. + if (preg_match('/^"(.+)"$/', $safe_display_name, $matches)) { + $safe_display_name = str_replace(['\\\\', '\\"'], ['\\', '"'], $matches[1]); + } + + // Transform the string in a RFC-2822 "quoted-string" by wrapping it in + // double-quotes. Also make sure '"' and '\' occurrences are escaped. + $safe_display_name = '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $safe_display_name) . '"'; + } + + return $safe_display_name; +} + /** * Wraps words on a single line. * diff --git a/modules/simpletest/tests/mail.test b/modules/simpletest/tests/mail.test index 3e40e13a89..307c77b29f 100644 --- a/modules/simpletest/tests/mail.test +++ b/modules/simpletest/tests/mail.test @@ -59,6 +59,81 @@ class MailTestCase extends DrupalWebTestCase implements MailSystemInterface { $this->assertNull(self::$sent_message, 'Message was canceled.'); } + /** + * Checks for the site name in an auto-generated From: header. + */ + function testFromHeader() { + global $language; + $default_from = variable_get('site_mail', ini_get('sendmail_from')); + $site_name = variable_get('site_name', 'Drupal'); + + // Reset the class variable holding a copy of the last sent message. + self::$sent_message = NULL; + // Send an e-mail with a sender address specified. + $from_email = 'someone_else@example.com'; + $message = drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language, array(), $from_email); + // Test that the from e-mail is just the e-mail and not the site name and + // default sender e-mail. + $this->assertEqual($from_email, self::$sent_message['headers']['From']); + + // Check default behavior is only email in FROM header. + self::$sent_message = NULL; + // Send an e-mail and check that the From-header contains only default mail address. + variable_del('mail_display_name_site_name'); + $message = drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language); + $this->assertEqual($default_from, self::$sent_message['headers']['From']); + + self::$sent_message = NULL; + // Send an e-mail and check that the From-header contains the site name. + variable_set('mail_display_name_site_name', TRUE); + $message = drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language); + $this->assertEqual($site_name . ' <' . $default_from . '>', self::$sent_message['headers']['From']); + } + + /** + * Checks for the site name in an auto-generated From: header. + */ + function testFromHeaderRfc2822Compliant() { + global $language; + $default_from = variable_get('site_mail', ini_get('sendmail_from')); + + // Enable adding a site name to From. + variable_set('mail_display_name_site_name', TRUE); + + $site_names = array( + // Simple ASCII characters. + 'Test site' => 'Test site', + // ASCII with html entity. + 'Test & site' => 'Test & site', + // Non-ASCII characters. + 'Tést site' => '=?UTF-8?B?VMOpc3Qgc2l0ZQ==?=', + // Non-ASCII with special characters. + 'Tést; site' => '=?UTF-8?B?VMOpc3Q7IHNpdGU=?=', + // Non-ASCII with html entity. + 'Tést; site' => '=?UTF-8?B?VMOpc3Q7IHNpdGU=?=', + // ASCII with special characters. + 'Test; site' => '"Test; site"', + // ASCII with special characters as html entity. + 'Test < site' => '"Test < site"', + // ASCII with special characters and '\'. + 'Test; \ "site"' => '"Test; \\\\ \"site\""', + // String already RFC-2822 compliant. + '"Test; site"' => '"Test; site"', + // String already RFC-2822 compliant. + '"Test; \\\\ \"site\""' => '"Test; \\\\ \"site\""', + ); + + foreach ($site_names as $original_name => $safe_string) { + variable_set('site_name', $original_name); + + // Reset the class variable holding a copy of the last sent message. + self::$sent_message = NULL; + // Send an e-mail and check that the From-header contains is RFC-2822 compliant. + drupal_mail('simpletest', 'from_test', 'from_test@example.com', $language); + $this->assertEqual($safe_string . ' <' . $default_from . '>', self::$sent_message['headers']['From']); + } + } + /** * Concatenate and wrap the e-mail body for plain-text mails. * diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 4881a51850..bf367b2089 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -701,3 +701,18 @@ $conf['file_scan_ignore_directories'] = array( * optimization and revert to the original behaviour. */ # $conf['variable_initialize_wait_for_lock'] = FALSE; + +/** + * Use site name as display-name in outgoing mail. + * + * Drupal can use the site name (i.e. the value of the site_name variable) as + * the display-name when sending e-mail. For example this would mean the sender + * might be "Acme Website" as opposed to just the e-mail + * address alone. In order to avoid disruption this is not enabled by default + * for existing sites. The feature can be enabled by setting this variable to + * TRUE. + * + * @see https://tools.ietf.org/html/rfc2822 + * @see drupal_mail() + */ +$conf['mail_display_name_site_name'] = TRUE;