diff --git a/core/core.services.yml b/core/core.services.yml
index 8527b2773abd0096fef71b46260a68fae25d0cb8..200972e73b32797b03efbfdc3d96c1a0e7667082 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 e62297c5e2ee81a1a55eb87d0b9e1e74a01bf921..c22d10acb571edad22608a063f6535b1d4bd05a0 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 @@
  *   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 0000000000000000000000000000000000000000..3f180ccf434b71e70de1c1a38673bc221a77cc74
--- /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 0daaf3a742a3b97a61fbe62b04a3a039cf4b3dac..48e53cfbb44623aa3048705374d2f1731397579b 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 @@
  * @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 @@ public function getInstance(array $options) {
     $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 @@ public function getInstance(array $options) {
         $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 0000000000000000000000000000000000000000..89916c1d614109cb567533057f1796b104679f33
--- /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
similarity index 61%
rename from core/modules/system/src/Tests/Mail/WrapMailUnitTest.php
rename to core/tests/Drupal/Tests/Core/Mail/MailFormatHelperTest.php
index 08c3debffa987422917621dd0b3319638f1a99aa..f1f4258502353793ff7b47c279c61072c0f04899 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 @@ function testDrupalWrapMail() {
     $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 b40db1dcee4dac5fde771b736cfd9f6e2066b124..7d19b8a0a483bbb79dc4059a1e678900fecf4ea7 100644
--- a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php
@@ -44,6 +44,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.
    *
@@ -82,11 +89,15 @@ protected function setUp() {
    */
   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);
   }