summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2014-08-11 09:25:38 -0500
committerAlex Pott2014-08-11 09:25:38 -0500
commit6f476b8a43ee90680b4728b4cdb0d5765809ed76 (patch)
treec6d5521ec705f03f0c7df2cf61dc9c9d96f6c632
parent54691d45d4049d551f2a5f9861ecd66bbd25a1b5 (diff)
Issue #2301393 by kim.pepper, larowlan: Deprecate all of mail.inc, move drupal_mail to method on Mail plugin manager.
-rw-r--r--core/core.services.yml2
-rw-r--r--core/includes/mail.inc374
-rw-r--r--core/lib/Drupal/Core/Mail/MailFormatHelper.php395
-rw-r--r--core/lib/Drupal/Core/Mail/MailManager.php125
-rw-r--r--core/lib/Drupal/Core/Mail/MailManagerInterface.php124
-rw-r--r--core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php (renamed from core/modules/system/src/Tests/Mail/WrapMailUnitTest.php)22
-rw-r--r--core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php19
7 files changed, 684 insertions, 377 deletions
diff --git a/core/core.services.yml b/core/core.services.yml
index 8527b27..200972e 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -745,7 +745,7 @@ services:
- { name: backend_overridable }
plugin.manager.mail:
class: Drupal\Core\Mail\MailManager
- arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@config.factory']
+ arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@config.factory', '@logger.factory', '@string_translation']
plugin.manager.condition:
class: Drupal\Core\Condition\ConditionManager
parent: default_plugin_manager
diff --git a/core/includes/mail.inc b/core/includes/mail.inc
index e62297c..c22d10a 100644
--- a/core/includes/mail.inc
+++ b/core/includes/mail.inc
@@ -5,9 +5,7 @@
* API functions for processing and sending email.
*/
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Xss;
-use Drupal\Core\Site\Settings;
+use Drupal\Core\Mail\MailFormatHelper;
/**
* Composes and optionally sends an email message.
@@ -108,89 +106,20 @@ use Drupal\Core\Site\Settings;
* implementing hook_mail_alter() may cancel sending by setting
* $message['send'] to FALSE.
*
- * @return
+ * @return array
* The $message array structure containing all details of the
* message. If already sent ($send = TRUE), then the 'result' element
* will contain the success indicator of the email, failure being already
* written to the watchdog. (Success means nothing more than the message being
* accepted at php-level, which still doesn't guarantee it to be delivered.)
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ * Use \Drupal::service('plugin.manager.mail')->mail() in procedural code. In
+ * Object-Oriented code inject the 'plugin.manager.mail' service and use the
+ * ::mail() method.
*/
function drupal_mail($module, $key, $to, $langcode, $params = array(), $reply = NULL, $send = TRUE) {
- $site_config = \Drupal::config('system.site');
- $site_mail = $site_config->get('mail');
- if (empty($site_mail)) {
- $site_mail = ini_get('sendmail_from');
- }
-
- // Bundle up the variables into a structured array for altering.
- $message = array(
- 'id' => $module . '_' . $key,
- 'module' => $module,
- 'key' => $key,
- 'to' => $to,
- 'from' => $site_mail,
- 'reply-to' => $reply,
- 'langcode' => $langcode,
- 'params' => $params,
- 'send' => TRUE,
- 'subject' => '',
- 'body' => array()
- );
-
- // Build the default headers
- $headers = array(
- 'MIME-Version' => '1.0',
- 'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
- 'Content-Transfer-Encoding' => '8Bit',
- 'X-Mailer' => 'Drupal'
- );
- // To prevent email from looking like spam, the addresses in the Sender and
- // Return-Path headers should have a domain authorized to use the
- // originating SMTP server.
- $headers['Sender'] = $headers['Return-Path'] = $site_mail;
- $headers['From'] = $site_config->get('name') . ' <' . $site_mail . '>';
- if ($reply) {
- $headers['Reply-to'] = $reply;
- }
- $message['headers'] = $headers;
-
- // Build the email (get subject and body, allow additional headers) by
- // invoking hook_mail() on this module. We cannot use
- // moduleHandler()->invoke() as we need to have $message by reference in
- // hook_mail().
- if (function_exists($function = $module . '_mail')) {
- $function($key, $message, $params);
- }
-
- // Invoke hook_mail_alter() to allow all modules to alter the resulting email.
- \Drupal::moduleHandler()->alter('mail', $message);
-
- // Retrieve the responsible implementation for this message.
- $system = drupal_mail_system($module, $key);
-
- // Format the message body.
- $message = $system->format($message);
-
- // Optionally send email.
- if ($send) {
- // The original caller requested sending. Sending was canceled by one or
- // more hook_mail_alter() implementations. We set 'result' to NULL, because
- // FALSE indicates an error in sending.
- if (empty($message['send'])) {
- $message['result'] = NULL;
- }
- // Sending was originally requested and was not canceled.
- else {
- $message['result'] = $system->mail($message);
- // Log errors.
- if (!$message['result']) {
- \Drupal::logger('mail')->error('Error sending email (from %from to %to with reply-to %reply).', array('%from' => $message['from'], '%to' => $message['to'], '%reply' => $message['reply-to'] ? $message['reply-to'] : t('not set')));
- drupal_set_message(t('Unable to send email. Contact the site administrator if the problem persists.'), 'error');
- }
- }
- }
-
- return $message;
+ return \Drupal::service('plugin.manager.mail')->mail($module, $key, $to, $langcode, $params, $reply, $send);
}
/**
@@ -208,6 +137,11 @@ function drupal_mail($module, $key, $to, $langcode, $params = array(), $reply =
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*
* @see \Drupal\Core\Mail\MailManager::getInstance()
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ * Use \Drupal::service('plugin.manager.mail')->getInstance() in procedural
+ * code. In Object-Oriented code inject the 'plugin.manager.mail' service and
+ * use the ::getInstance() method.
*/
function drupal_mail_system($module, $key) {
return \Drupal::service('plugin.manager.mail')->getInstance(array('module' => $module, 'key' => $key));
@@ -229,35 +163,12 @@ function drupal_mail_system($module, $key) {
*
* @return
* The content of the email as a string with formatting applied.
+ *
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ * Use \Drupal\Core\Utility\Mail::wrapMail().
*/
function drupal_wrap_mail($text, $indent = '') {
- // Convert CRLF into LF.
- $text = str_replace("\r", '', $text);
- // See if soft-wrapping is allowed.
- $clean_indent = _drupal_html_to_text_clean($indent);
- $soft = strpos($clean_indent, ' ') === FALSE;
- // Check if the string has line breaks.
- if (strpos($text, "\n") !== FALSE) {
- // Remove trailing spaces to make existing breaks hard, but leave signature
- // marker untouched (RFC 3676, Section 4.3).
- $text = preg_replace('/(?(?<!^--) +\n| +\n)/m', "\n", $text);
- // Wrap each line at the needed width.
- $lines = explode("\n", $text);
- array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent)));
- $text = implode("\n", $lines);
- }
- else {
- // Wrap this line.
- _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => strlen($indent)));
- }
- // Empty lines with nothing but spaces.
- $text = preg_replace('/^ +\n/m', "\n", $text);
- // Space-stuff special lines.
- $text = preg_replace('/^(>| |From)/m', ' $1', $text);
- // Apply indentation. We only include non-'>' indentation on the first line.
- $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent));
-
- return $text;
+ return MailFormatHelper::wrapMail($text, $indent);
}
/**
@@ -280,253 +191,10 @@ function drupal_wrap_mail($text, $indent = '') {
*
* @return
* The transformed string.
- */
-function drupal_html_to_text($string, $allowed_tags = NULL) {
- // Cache list of supported tags.
- static $supported_tags;
- if (empty($supported_tags)) {
- $supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr');
- }
-
- // Make sure only supported tags are kept.
- $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags;
-
- // Make sure tags, entities and attributes are well-formed and properly nested.
- $string = Html::normalize(Xss::filter($string, $allowed_tags));
-
- // Apply inline styles.
- $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
- $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
-
- // Replace inline <a> tags with the text of link and a footnote.
- // 'See <a href="http://drupal.org">the Drupal site</a>' becomes
- // 'See the Drupal site [1]' with the URL included as a footnote.
- _drupal_html_to_mail_urls(NULL, TRUE);
- $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
- $string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls', $string);
- $urls = _drupal_html_to_mail_urls();
- $footnotes = '';
- if (count($urls)) {
- $footnotes .= "\n";
- for ($i = 0, $max = count($urls); $i < $max; $i++) {
- $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
- }
- }
-
- // Split tags from text.
- $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
- // Note: PHP ensures the array consists of alternating delimiters and literals
- // and begins and ends with a literal (inserting $null as required).
-
- $tag = FALSE; // Odd/even counter (tag or no tag)
- $casing = NULL; // Case conversion function
- $output = '';
- $indent = array(); // All current indentation string chunks
- $lists = array(); // Array of counters for opened lists
- foreach ($split as $value) {
- $chunk = NULL; // Holds a string ready to be formatted and output.
-
- // Process HTML tags (but don't output any literally).
- if ($tag) {
- list($tagname) = explode(' ', strtolower($value), 2);
- switch ($tagname) {
- // List counters
- case 'ul':
- array_unshift($lists, '*');
- break;
- case 'ol':
- array_unshift($lists, 1);
- break;
- case '/ul':
- case '/ol':
- array_shift($lists);
- $chunk = ''; // Ensure blank new-line.
- break;
-
- // Quotation/list markers, non-fancy headers
- case 'blockquote':
- // Format=flowed indentation cannot be mixed with lists.
- $indent[] = count($lists) ? ' "' : '>';
- break;
- case 'li':
- $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * ';
- break;
- case 'dd':
- $indent[] = ' ';
- break;
- case 'h3':
- $indent[] = '.... ';
- break;
- case 'h4':
- $indent[] = '.. ';
- break;
- case '/blockquote':
- if (count($lists)) {
- // Append closing quote for inline quotes (immediately).
- $output = rtrim($output, "> \n") . "\"\n";
- $chunk = ''; // Ensure blank new-line.
- }
- // Fall-through
- case '/li':
- case '/dd':
- array_pop($indent);
- break;
- case '/h3':
- case '/h4':
- array_pop($indent);
- case '/h5':
- case '/h6':
- $chunk = ''; // Ensure blank new-line.
- break;
-
- // Fancy headers
- case 'h1':
- $indent[] = '======== ';
- $casing = 'drupal_strtoupper';
- break;
- case 'h2':
- $indent[] = '-------- ';
- $casing = 'drupal_strtoupper';
- break;
- case '/h1':
- case '/h2':
- $casing = NULL;
- // Pad the line with dashes.
- $output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ? '=' : '-', ' ');
- array_pop($indent);
- $chunk = ''; // Ensure blank new-line.
- break;
-
- // Horizontal rulers
- case 'hr':
- // Insert immediately.
- $output .= drupal_wrap_mail('', implode('', $indent)) . "\n";
- $output = _drupal_html_to_text_pad($output, '-');
- break;
-
- // Paragraphs and definition lists
- case '/p':
- case '/dl':
- $chunk = ''; // Ensure blank new-line.
- break;
- }
- }
- // Process blocks of text.
- else {
- // Convert inline HTML text to plain text; not removing line-breaks or
- // white-space, since that breaks newlines when sanitizing plain-text.
- $value = trim(decode_entities($value));
- if (drupal_strlen($value)) {
- $chunk = $value;
- }
- }
-
- // See if there is something waiting to be output.
- if (isset($chunk)) {
- // Apply any necessary case conversion.
- if (isset($casing)) {
- $chunk = $casing($chunk);
- }
- $line_endings = Settings::get('mail_line_endings', PHP_EOL);
- // Format it and apply the current indentation.
- $output .= drupal_wrap_mail($chunk, implode('', $indent)) . $line_endings;
- // Remove non-quotation markers from indentation.
- $indent = array_map('_drupal_html_to_text_clean', $indent);
- }
-
- $tag = !$tag;
- }
-
- return $output . $footnotes;
-}
-
-/**
- * Wraps words on a single line.
- *
- * Callback for array_walk() winthin drupal_wrap_mail().
- *
- * Note that we are skipping MIME content header lines, because attached files,
- * especially applications, could have long MIME types or long filenames which
- * result in line length longer than the 77 characters limit and wrapping that
- * line will break the email format. E.g., the attached file hello_drupal.docx
- * will produce the following Content-Type:
- * Content-Type:
- * application/vnd.openxmlformats-officedocument.wordprocessingml.document;
- * name="hello_drupal.docx"
- */
-function _drupal_wrap_mail_line(&$line, $key, $values) {
- $line_is_mime_header = FALSE;
- $mime_headers = array(
- 'Content-Type',
- 'Content-Transfer-Encoding',
- 'Content-Disposition',
- 'Content-Description',
- );
-
- // Do not break MIME headers which could be longer than 77 characters.
- foreach ($mime_headers as $header) {
- if (strpos($line, $header . ': ') === 0) {
- $line_is_mime_header = TRUE;
- break;
- }
- }
- if (!$line_is_mime_header) {
- // Use soft-breaks only for purely quoted or unindented text.
- $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n");
- }
- // Break really long words at the maximum width allowed.
- $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n");
-}
-
-/**
- * Keeps track of URLs and replaces them with placeholder tokens.
*
- * Callback for preg_replace_callback() within drupal_html_to_text().
+ * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
+ * Use \Drupal\Core\Utility\Mail::htmlToText().
*/
-function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) {
- global $base_url, $base_path;
- static $urls = array(), $regexp;
-
- if ($reset) {
- // Reset internal URL list.
- $urls = array();
- }
- else {
- if (empty($regexp)) {
- $regexp = '@^' . preg_quote($base_path, '@') . '@';
- }
- if ($match) {
- list(, , $url, $label) = $match;
- // Ensure all URLs are absolute.
- $urls[] = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url);
- return $label . ' [' . count($urls) . ']';
- }
- }
- return $urls;
-}
-
-/**
- * Replaces non-quotation markers from a given piece of indentation with spaces.
- *
- * Callback for array_map() within drupal_html_to_text().
- */
-function _drupal_html_to_text_clean($indent) {
- return preg_replace('/[^>]/', ' ', $indent);
-}
-
-/**
- * Pads the last line with the given character.
- *
- * @see drupal_html_to_text()
- */
-function _drupal_html_to_text_pad($text, $pad, $prefix = '') {
- // Remove last line break.
- $text = substr($text, 0, -1);
- // Calculate needed padding space and add it.
- if (($p = strrpos($text, "\n")) === FALSE) {
- $p = -1;
- }
- $n = max(0, 79 - (strlen($text) - $p) - strlen($prefix));
- // Add prefix and padding, and restore linebreak.
- return $text . $prefix . str_repeat($pad, $n) . "\n";
+function drupal_html_to_text($string, $allowed_tags = NULL) {
+ return MailFormatHelper::htmlToText($string, $allowed_tags);
}
diff --git a/core/lib/Drupal/Core/Mail/MailFormatHelper.php b/core/lib/Drupal/Core/Mail/MailFormatHelper.php
new file mode 100644
index 0000000..3f180cc
--- /dev/null
+++ b/core/lib/Drupal/Core/Mail/MailFormatHelper.php
@@ -0,0 +1,395 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Mail\MailFormatHelper.
+ */
+
+namespace Drupal\Core\Mail;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Site\Settings;
+
+/**
+ * Defines a class containing utility methods for formatting mail messages.
+ */
+class MailFormatHelper {
+
+ /**
+ * Internal array of urls replaced with tokens.
+ *
+ * @var array
+ */
+ protected static $urls = array();
+
+ /**
+ * Quoted regex expression based on base path.
+ *
+ * @var string
+ */
+ protected static $regexp;
+
+ /**
+ * Array of tags supported.
+ *
+ * @var array
+ */
+ protected static $supportedTags = array();
+
+ /**
+ * Performs format=flowed soft wrapping for mail (RFC 3676).
+ *
+ * We use delsp=yes wrapping, but only break non-spaced languages when
+ * absolutely necessary to avoid compatibility issues.
+ *
+ * We deliberately use LF rather than CRLF, see MailManagerInterface::mail().
+ *
+ * @param string $text
+ * The plain text to process.
+ * @param string $indent
+ * (optional) A string to indent the text with. Only '>' characters are
+ * repeated on subsequent wrapped lines. Others are replaced by spaces.
+ *
+ * @return string
+ * The content of the email as a string with formatting applied.
+ */
+ public static function wrapMail($text, $indent = '') {
+ // Convert CRLF into LF.
+ $text = str_replace("\r", '', $text);
+ // See if soft-wrapping is allowed.
+ $clean_indent = static::htmlToTextClean($indent);
+ $soft = strpos($clean_indent, ' ') === FALSE;
+ // Check if the string has line breaks.
+ if (strpos($text, "\n") !== FALSE) {
+ // Remove trailing spaces to make existing breaks hard, but leave
+ // signature marker untouched (RFC 3676, Section 4.3).
+ $text = preg_replace('/(?(?<!^--) +\n| +\n)/m', "\n", $text);
+ // Wrap each line at the needed width.
+ $lines = explode("\n", $text);
+ array_walk($lines, '\Drupal\Core\Mail\MailFormatHelper::wrapMailLine', array('soft' => $soft, 'length' => strlen($indent)));
+ $text = implode("\n", $lines);
+ }
+ else {
+ // Wrap this line.
+ static::wrapMailLine($text, 0, array('soft' => $soft, 'length' => strlen($indent)));
+ }
+ // Empty lines with nothing but spaces.
+ $text = preg_replace('/^ +\n/m', "\n", $text);
+ // Space-stuff special lines.
+ $text = preg_replace('/^(>| |From)/m', ' $1', $text);
+ // Apply indentation. We only include non-'>' indentation on the first line.
+ $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent));
+
+ return $text;
+ }
+
+ /**
+ * Transforms an HTML string into plain text, preserving its structure.
+ *
+ * The output will be suitable for use as 'format=flowed; delsp=yes' text
+ * (RFC 3676) and can be passed directly to MailManagerInterface::mail() for sending.
+ *
+ * We deliberately use LF rather than CRLF, see MailManagerInterface::mail().
+ *
+ * This function provides suitable alternatives for the following tags:
+ * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt>
+ * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr>
+ *
+ * @param string $string
+ * The string to be transformed.
+ * @param array $allowed_tags
+ * (optional) If supplied, a list of tags that will be transformed. If
+ * omitted, all supported tags are transformed.
+ *
+ * @return string
+ * The transformed string.
+ */
+ public static function htmlToText($string, $allowed_tags = NULL) {
+ // Cache list of supported tags.
+ if (empty(static::$supportedTags)) {
+ static::$supportedTags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p',
+ 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3',
+ 'h4', 'h5', 'h6', 'hr');
+ }
+
+ // Make sure only supported tags are kept.
+ $allowed_tags = isset($allowed_tags) ? array_intersect(static::$supportedTags, $allowed_tags) : static::$supportedTags;
+
+ // Make sure tags, entities and attributes are well-formed and properly
+ // nested.
+ $string = Html::normalize(Xss::filter($string, $allowed_tags));
+
+ // Apply inline styles.
+ $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
+ $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
+
+ // Replace inline <a> tags with the text of link and a footnote.
+ // 'See <a href="http://drupal.org">the Drupal site</a>' becomes
+ // 'See the Drupal site [1]' with the URL included as a footnote.
+ static::htmlToMailUrls(NULL, TRUE);
+ $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
+ $string = preg_replace_callback($pattern, 'static::htmlToMailUrls', $string);
+ $urls = static::htmlToMailUrls();
+ $footnotes = '';
+ if (count($urls)) {
+ $footnotes .= "\n";
+ for ($i = 0, $max = count($urls); $i < $max; $i++) {
+ $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
+ }
+ }
+
+ // Split tags from text.
+ $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // Note: PHP ensures the array consists of alternating delimiters and
+ // literals and begins and ends with a literal (inserting $null as
+ // required).
+ // Odd/even counter (tag or no tag).
+ $tag = FALSE;
+ // Case conversion function.
+ $casing = NULL;
+ $output = '';
+ // All current indentation string chunks.
+ $indent = array();
+ // Array of counters for opened lists.
+ $lists = array();
+ foreach ($split as $value) {
+ // Holds a string ready to be formatted and output.
+ $chunk = NULL;
+
+ // Process HTML tags (but don't output any literally).
+ if ($tag) {
+ list($tagname) = explode(' ', strtolower($value), 2);
+ switch ($tagname) {
+ // List counters.
+ case 'ul':
+ array_unshift($lists, '*');
+ break;
+
+ case 'ol':
+ array_unshift($lists, 1);
+ break;
+
+ case '/ul':
+ case '/ol':
+ array_shift($lists);
+ // Ensure blank new-line.
+ $chunk = '';
+ break;
+
+ // Quotation/list markers, non-fancy headers.
+ case 'blockquote':
+ // Format=flowed indentation cannot be mixed with lists.
+ $indent[] = count($lists) ? ' "' : '>';
+ break;
+
+ case 'li':
+ $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * ';
+ break;
+
+ case 'dd':
+ $indent[] = ' ';
+ break;
+
+ case 'h3':
+ $indent[] = '.... ';
+ break;
+
+ case 'h4':
+ $indent[] = '.. ';
+ break;
+
+ case '/blockquote':
+ if (count($lists)) {
+ // Append closing quote for inline quotes (immediately).
+ $output = rtrim($output, "> \n") . "\"\n";
+ // Ensure blank new-line.
+ $chunk = '';
+ }
+
+ // Fall-through.
+ case '/li':
+ case '/dd':
+ array_pop($indent);
+ break;
+
+ case '/h3':
+ case '/h4':
+ array_pop($indent);
+ case '/h5':
+ case '/h6':
+ // Ensure blank new-line.
+ $chunk = '';
+ break;
+
+ // Fancy headers.
+ case 'h1':
+ $indent[] = '======== ';
+ $casing = 'drupal_strtoupper';
+ break;
+
+ case 'h2':
+ $indent[] = '-------- ';
+ $casing = 'drupal_strtoupper';
+ break;
+
+ case '/h1':
+ case '/h2':
+ $casing = NULL;
+ // Pad the line with dashes.
+ $output = static::htmlToTextPad($output, ($tagname == '/h1') ? '=' : '-', ' ');
+ array_pop($indent);
+ // Ensure blank new-line.
+ $chunk = '';
+ break;
+
+ // Horizontal rulers.
+ case 'hr':
+ // Insert immediately.
+ $output .= static::wrapMail('', implode('', $indent)) . "\n";
+ $output = static::htmlToTextPad($output, '-');
+ break;
+
+ // Paragraphs and definition lists.
+ case '/p':
+ case '/dl':
+ // Ensure blank new-line.
+ $chunk = '';
+ break;
+ }
+ }
+ // Process blocks of text.
+ else {
+ // Convert inline HTML text to plain text; not removing line-breaks or
+ // white-space, since that breaks newlines when sanitizing plain-text.
+ $value = trim(decode_entities($value));
+ if (drupal_strlen($value)) {
+ $chunk = $value;
+ }
+ }
+
+ // See if there is something waiting to be output.
+ if (isset($chunk)) {
+ // Apply any necessary case conversion.
+ if (isset($casing)) {
+ $chunk = $casing($chunk);
+ }
+ $line_endings = Settings::get('mail_line_endings', PHP_EOL);
+ // Format it and apply the current indentation.
+ $output .= static::wrapMail($chunk, implode('', $indent)) . $line_endings;
+ // Remove non-quotation markers from indentation.
+ $indent = array_map('\Drupal\Core\Mail\MailFormatHelper::htmlToTextClean', $indent);
+ }
+
+ $tag = !$tag;
+ }
+
+ return $output . $footnotes;
+ }
+
+ /**
+ * Wraps words on a single line.
+ *
+ * Callback for array_walk() within
+ * \Drupal\Core\Mail\MailFormatHelper::wrapMail().
+ *
+ * Note that we are skipping MIME content header lines, because attached
+ * files, especially applications, could have long MIME types or long
+ * filenames which result in line length longer than the 77 characters limit
+ * and wrapping that line will break the email format. E.g., the attached file
+ * hello_drupal.docx will produce the following Content-Type:
+ * @code
+ * Content-Type:
+ * application/vnd.openxmlformats-officedocument.wordprocessingml.document;
+ * name="hello_drupal.docx"
+ * @endcode
+ */
+ protected static function wrapMailLine(&$line, $key, $values) {
+ $line_is_mime_header = FALSE;
+ $mime_headers = array(
+ 'Content-Type',
+ 'Content-Transfer-Encoding',
+ 'Content-Disposition',
+ 'Content-Description',
+ );
+
+ // Do not break MIME headers which could be longer than 77 characters.
+ foreach ($mime_headers as $header) {
+ if (strpos($line, $header . ': ') === 0) {
+ $line_is_mime_header = TRUE;
+ break;
+ }
+ }
+ if (!$line_is_mime_header) {
+ // Use soft-breaks only for purely quoted or unindented text.
+ $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n");
+ }
+ // Break really long words at the maximum width allowed.
+ $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n");
+ }
+
+ /**
+ * Keeps track of URLs and replaces them with placeholder tokens.
+ *
+ * Callback for preg_replace_callback() within
+ * \Drupal\Core\Mail\MailFormatHelper::htmlToText().
+ */
+ protected static function htmlToMailUrls($match = NULL, $reset = FALSE) {
+ // @todo Use request context instead.
+ global $base_url, $base_path;
+
+ if ($reset) {
+ // Reset internal URL list.
+ static::$urls = array();
+ }
+ else {
+ if (empty(static::$regexp)) {
+ static::$regexp = '@^' . preg_quote($base_path, '@') . '@';
+ }
+ if ($match) {
+ list(, , $url, $label) = $match;
+ // Ensure all URLs are absolute.
+ static::$urls[] = strpos($url, '://') ? $url : preg_replace(static::$regexp, $base_url . '/', $url);
+ return $label . ' [' . count(static::$urls) . ']';
+ }
+ }
+ return static::$urls;
+ }
+
+ /**
+ * Replaces non-quotation markers from a piece of indentation with spaces.
+ *
+ * Callback for array_map() within
+ * \Drupal\Core\Mail\MailFormatHelper::htmlToText().
+ */
+ protected static function htmlToTextClean($indent) {
+ return preg_replace('/[^>]/', ' ', $indent);
+ }
+
+ /**
+ * Pads the last line with the given character.
+ *
+ * @param string $text
+ * The text to pad.
+ * @param string $pad
+ * The character to pad the end of the string with.
+ * @param string $prefix
+ * (optional) Prefix to add to the string.
+ *
+ * @return string
+ * The padded string.
+ *
+ * @see \Drupal\Core\Mail\MailFormatHelper::htmlToText()
+ */
+ protected static function htmlToTextPad($text, $pad, $prefix = '') {
+ // Remove last line break.
+ $text = substr($text, 0, -1);
+ // Calculate needed padding space and add it.
+ if (($p = strrpos($text, "\n")) === FALSE) {
+ $p = -1;
+ }
+ $n = max(0, 79 - (strlen($text) - $p) - strlen($prefix));
+ // Add prefix and padding, and restore linebreak.
+ return $text . $prefix . str_repeat($pad, $n) . "\n";
+ }
+}
diff --git a/core/lib/Drupal/Core/Mail/MailManager.php b/core/lib/Drupal/Core/Mail/MailManager.php
index 0daaf3a..48e53cf 100644
--- a/core/lib/Drupal/Core/Mail/MailManager.php
+++ b/core/lib/Drupal/Core/Mail/MailManager.php
@@ -7,12 +7,15 @@
namespace Drupal\Core\Mail;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Utility\String;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides a Mail plugin manager.
@@ -21,14 +24,23 @@ use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
* @see \Drupal\Core\Mail\MailInterface
* @see plugin_api
*/
-class MailManager extends DefaultPluginManager {
+class MailManager extends DefaultPluginManager implements MailManagerInterface {
+
+ use StringTranslationTrait;
/**
- * Config object for mail system configurations.
+ * The config factory.
*
- * @var \Drupal\Core\Config\Config
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
*/
- protected $mailConfig;
+ protected $configFactory;
+
+ /**
+ * The logger factory.
+ *
+ * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+ */
+ protected $loggerFactory;
/**
* List of already instantiated mail plugins.
@@ -49,12 +61,18 @@ class MailManager extends DefaultPluginManager {
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
+ * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+ * The logger channel factory.
+ * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+ * The string translation service.
*/
- public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) {
+ public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, TranslationInterface $string_translation) {
parent::__construct('Plugin/Mail', $namespaces, $module_handler, 'Drupal\Core\Annotation\Mail');
$this->alterInfo('mail_backend_info');
$this->setCacheBackend($cache_backend, 'mail_backend_plugins');
- $this->mailConfig = $config_factory->get('system.mail');
+ $this->configFactory = $config_factory;
+ $this->loggerFactory = $logger_factory;
+ $this->stringTranslation = $string_translation;
}
/**
@@ -116,7 +134,7 @@ class MailManager extends DefaultPluginManager {
$key = $options['key'];
$message_id = $module . '_' . $key;
- $configuration = $this->mailConfig->get('interface');
+ $configuration = $this->configFactory->get('system.mail')->get('interface');
// Look for overrides for the default mail plugin, starting from the most
// specific message_id, and falling back to the module name.
@@ -136,9 +154,100 @@ class MailManager extends DefaultPluginManager {
$this->instances[$plugin_id] = $plugin;
}
else {
- throw new InvalidPluginDefinitionException($plugin_id, String::format('Class %class does not implement interface %interface', array('%class' => get_class($plugin), '%interface' => 'Drupal\Core\Mail\MailInterface')));
+ throw new InvalidPluginDefinitionException($plugin_id, String::format('Class %class does not implement interface %interface', array(
+ '%class' => get_class($plugin),
+ '%interface' => 'Drupal\Core\Mail\MailInterface',
+ )));
}
}
return $this->instances[$plugin_id];
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mail($module, $key, $to, $langcode, $params = array(), $reply = NULL, $send = TRUE) {
+ $site_config = $this->configFactory->get('system.site');
+ $site_mail = $site_config->get('mail');
+ if (empty($site_mail)) {
+ $site_mail = ini_get('sendmail_from');
+ }
+
+ // Bundle up the variables into a structured array for altering.
+ $message = array(
+ 'id' => $module . '_' . $key,
+ 'module' => $module,
+ 'key' => $key,
+ 'to' => $to,
+ 'from' => $site_mail,
+ 'reply-to' => $reply,
+ 'langcode' => $langcode,
+ 'params' => $params,
+ 'send' => TRUE,
+ 'subject' => '',
+ 'body' => array(),
+ );
+
+ // Build the default headers.
+ $headers = array(
+ 'MIME-Version' => '1.0',
+ 'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
+ 'Content-Transfer-Encoding' => '8Bit',
+ 'X-Mailer' => 'Drupal',
+ );
+ // To prevent email from looking like spam, the addresses in the Sender and
+ // Return-Path headers should have a domain authorized to use the
+ // originating SMTP server.
+ $headers['Sender'] = $headers['Return-Path'] = $site_mail;
+ $headers['From'] = $site_config->get('name') . ' <' . $site_mail . '>';
+ if ($reply) {
+ $headers['Reply-to'] = $reply;
+ }
+ $message['headers'] = $headers;
+
+ // Build the email (get subject and body, allow additional headers) by
+ // invoking hook_mail() on this module. We cannot use
+ // moduleHandler()->invoke() as we need to have $message by reference in
+ // hook_mail().
+ if (function_exists($function = $module . '_mail')) {
+ $function($key, $message, $params);
+ }
+
+ // Invoke hook_mail_alter() to allow all modules to alter the resulting
+ // email.
+ $this->moduleHandler->alter('mail', $message);
+
+ // Retrieve the responsible implementation for this message.
+ $system = $this->getInstance(array('module' => $module, 'key' => $key));
+
+ // Format the message body.
+ $message = $system->format($message);
+
+ // Optionally send email.
+ if ($send) {
+ // The original caller requested sending. Sending was canceled by one or
+ // more hook_mail_alter() implementations. We set 'result' to NULL,
+ // because FALSE indicates an error in sending.
+ if (empty($message['send'])) {
+ $message['result'] = NULL;
+ }
+ // Sending was originally requested and was not canceled.
+ else {
+ $message['result'] = $system->mail($message);
+ // Log errors.
+ if (!$message['result']) {
+ $this->loggerFactory->get('mail')
+ ->error('Error sending email (from %from to %to with reply-to %reply).', array(
+ '%from' => $message['from'],
+ '%to' => $message['to'],
+ '%reply' => $message['reply-to'] ? $message['reply-to'] : $this->t('not set'),
+ ));
+ drupal_set_message($this->t('Unable to send email. Contact the site administrator if the problem persists.'), 'error');
+ }
+ }
+ }
+
+ return $message;
+ }
+
}
diff --git a/core/lib/Drupal/Core/Mail/MailManagerInterface.php b/core/lib/Drupal/Core/Mail/MailManagerInterface.php
new file mode 100644
index 0000000..89916c1
--- /dev/null
+++ b/core/lib/Drupal/Core/Mail/MailManagerInterface.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Mail\MailManagerInterface.
+ */
+
+namespace Drupal\Core\Mail;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Provides an interface for sending mail.
+ */
+interface MailManagerInterface extends PluginManagerInterface {
+
+ /**
+ * Composes and optionally sends an email message.
+ *
+ * Sending an email works with defining an email template (subject, text and
+ * possibly email headers) and the replacement values to use in the
+ * appropriate places in the template. Processed email templates are requested
+ * from hook_mail() from the module sending the email. Any module can modify
+ * the composed email message array using hook_mail_alter(). Finally
+ * drupal_mail_system()->mail() sends the email, which can be reused if the
+ * exact same composed email is to be sent to multiple recipients.
+ *
+ * Finding out what language to send the email with needs some consideration.
+ * If you send email to a user, her preferred language should be fine, so use
+ * user_preferred_langcode(). If you send email based on form values filled on
+ * the page, there are two additional choices if you are not sending the email
+ * to a user on the site. You can either use the language used to generate the
+ * page or the site default language. See language_default(). The former is
+ * good if sending email to the person filling the form, the later is good if
+ * you send email to an address previously set up (like contact addresses in a
+ * contact form).
+ *
+ * Taking care of always using the proper language is even more important when
+ * sending emails in a row to multiple users. Hook_mail() abstracts whether
+ * the mail text comes from an administrator setting or is static in the
+ * source code. It should also deal with common mail tokens, only receiving
+ * $params which are unique to the actual email at hand.
+ *
+ * An example:
+ *
+ * @code
+ * function example_notify($accounts) {
+ * foreach ($accounts as $account) {
+ * $params['account'] = $account;
+ * // example_mail() will be called based on the first \Drupal::service('plugin.manager.mail')->mail() parameter.
+ * \Drupal::service('plugin.manager.mail')->mail('example', 'notice', $account->mail, user_preferred_langcode($account), $params);
+ * }
+ * }
+ *
+ * function example_mail($key, &$message, $params) {
+ * $data['user'] = $params['account'];
+ * $options['langcode'] = $message['langcode'];
+ * user_mail_tokens($variables, $data, $options);
+ * switch($key) {
+ * case 'notice':
+ * // If the recipient can receive such notices by instant-message, do
+ * // not send by email.
+ * if (example_im_send($key, $message, $params)) {
+ * $message['send'] = FALSE;
+ * break;
+ * }
+ * $message['subject'] = t('Notification from !site', $variables, $options);
+ * $message['body'][] = t("Dear !username\n\nThere is new content available on the site.", $variables, $options);
+ * break;
+ * }
+ * }
+ * @endcode
+ *
+ * Another example, which uses \Drupal::service('plugin.manager.mail')->mail()
+ * to format a message for sending later:
+ *
+ * @code
+ * $params = array('current_conditions' => $data);
+ * $to = 'user@example.com';
+ * $message = \Drupal::service('plugin.manager.mail')->mail('example', 'notice', $to, $langcode, $params, FALSE);
+ * // Only add to the spool if sending was not canceled.
+ * if ($message['send']) {
+ * example_spool_message($message);
+ * }
+ * @endcode
+ *
+ * @param string $module
+ * A module name to invoke hook_mail() on. The {$module}_mail() hook will be
+ * called to complete the $message structure which will already contain
+ * common defaults.
+ * @param string $key
+ * A key to identify the email sent. The final message ID for email altering
+ * will be {$module}_{$key}.
+ * @param string $to
+ * The email address or addresses where the message will be sent to. The
+ * formatting of this string will be validated with the
+ * @link http://php.net/manual/filter.filters.validate.php PHP email validation filter. @endlink
+ * Some examples are:
+ * - user@example.com
+ * - user@example.com, anotheruser@example.com
+ * - User <user@example.com>
+ * - User <user@example.com>, Another User <anotheruser@example.com>
+ * @param string $langcode
+ * Language code to use to compose the email.
+ * @param array $params
+ * (optional) Parameters to build the email.
+ * @param string|null $reply
+ * Optional email address to be used to answer.
+ * @param bool $send
+ * If TRUE, \Drupal::service('plugin.manager.mail')->mail() will call
+ * drupal_mail_system()->mail() to deliver the message, and store the result
+ * in $message['result']. Modules implementing hook_mail_alter() may cancel
+ * sending by setting $message['send'] to FALSE.
+ *
+ * @return string
+ * The $message array structure containing all details of the message. If
+ * already sent ($send = TRUE), then the 'result' element will contain the
+ * success indicator of the email, failure being already written to the
+ * watchdog. (Success means nothing more than the message being accepted at
+ * php-level, which still doesn't guarantee it to be delivered.)
+ */
+ public function mail($module, $key, $to, $langcode, $params = array(), $reply = NULL, $send = TRUE);
+
+}
diff --git a/core/modules/system/src/Tests/Mail/WrapMailUnitTest.php b/core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php
index 08c3deb..f1f4258 100644
--- a/core/modules/system/src/Tests/Mail/WrapMailUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php
@@ -2,24 +2,24 @@
/**
* @file
- * Definition of Drupal\system\Tests\Mail\WrapMailUnitTest.
+ * Contains Drupal\Tests\Core\Mail\MailFormatHelperTest.
*/
-namespace Drupal\system\Tests\Mail;
+namespace Drupal\Tests\Core\Mail;
-use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Mail\MailFormatHelper;
+use Drupal\Tests\UnitTestCase;
/**
- * Tests drupal_wrap_mail().
- *
+ * @coversDefaultClass \Drupal\Core\Mail\MailFormatHelper
* @group Mail
*/
-class WrapMailUnitTest extends UnitTestBase {
+class MailFormatHelperTest extends UnitTestCase {
/**
* Makes sure that drupal_wrap_mail() wraps the correct types of lines.
*/
- function testDrupalWrapMail() {
+ public function testWrapMail() {
$delimiter = "End of header\n";
$long_file_name = $this->randomMachineName(64) . '.docx';
$headers_in_body = 'Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="' . $long_file_name . "\"\n";
@@ -27,14 +27,14 @@ class WrapMailUnitTest extends UnitTestBase {
$headers_in_body .= 'Content-Disposition: attachment; filename="' . $long_file_name . "\"\n";
$headers_in_body .= 'Content-Description: ' . $this->randomMachineName(64);
$body = $this->randomMachineName(76) . ' ' . $this->randomMachineName(6);
- $wrapped_text = drupal_wrap_mail($headers_in_body . $delimiter . $body);
+ $wrapped_text = MailFormatHelper::wrapMail($headers_in_body . $delimiter . $body);
list($processed_headers, $processed_body) = explode($delimiter, $wrapped_text);
// Check that the body headers were not wrapped even though some exceeded
// 77 characters.
- $this->assertEqual($headers_in_body, $processed_headers, 'Headers in the body are not wrapped.');
+ $this->assertEquals($headers_in_body, $processed_headers, 'Headers in the body are not wrapped.');
// Check that the body text is wrapped.
- $this->assertEqual(wordwrap($body, 77, " \n"), $processed_body, 'Body text is wrapped.');
+ $this->assertEquals(wordwrap($body, 77, " \n"), $processed_body, 'Body text is wrapped.');
}
-}
+}
diff --git a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
index b40db1d..7d19b8a 100644
--- a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
@@ -45,6 +45,13 @@ class MailManagerTest extends UnitTestCase {
protected $discovery;
/**
+ * The mail manager under test.
+ *
+ * @var \Drupal\Tests\Core\Mail\TestMailManager
+ */
+ protected $mailManager;
+
+ /**
* A list of mail plugin definitions.
*
* @var array
@@ -82,11 +89,15 @@ class MailManagerTest extends UnitTestCase {
*/
protected function setUpMailManager($interface = array()) {
// Use the provided config for system.mail.interface settings.
- $this->configFactory = $this->getConfigFactoryStub(array('system.mail' => array(
- 'interface' => $interface,
- )));
+ $this->configFactory = $this->getConfigFactoryStub(array(
+ 'system.mail' => array(
+ 'interface' => $interface,
+ ),
+ ));
+ $logger_factory = $this->getMock('\Drupal\Core\Logger\LoggerChannelFactoryInterface');
+ $string_translation = $this->getStringTranslationStub();
// Construct the manager object and override its discovery.
- $this->mailManager = new TestMailManager(new \ArrayObject(), $this->cache, $this->moduleHandler, $this->configFactory);
+ $this->mailManager = new TestMailManager(new \ArrayObject(), $this->cache, $this->moduleHandler, $this->configFactory, $logger_factory, $string_translation);
$this->mailManager->setDiscovery($this->discovery);
}