diff --git a/html_to_text.inc b/html_to_text.inc
new file mode 100644
index 0000000..c88b7bf
--- /dev/null
+++ b/html_to_text.inc
@@ -0,0 +1,386 @@
+<?php
+
+/**
+ * Perform 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 variable_get('mail_line_endings), MAIL_LINE_ENDINGS)
+ * rather than "\r\n".
+ *
+ * @param $text
+ *   The plain text to process.
+ * @param $indent (optional)
+ *   A string to indent the text with. Only '>' characters are repeated on
+ *   subsequent wrapped lines. Others are replaced by spaces.
+ *
+ * @see drupal_mail()
+ */
+function simpletest_wrap_mail($text, $indent = '') {
+  // Convert CRLF into MAIL_LINE_ENDINGS.
+  $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
+  $text = preg_replace('/\r?\n/', $eol, $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, $eol) !== FALSE) {
+    // Remove trailing spaces to make existing breaks hard.
+    $text = preg_replace('/ +\r?\n/m', $eol, $text);
+    // Wrap each line at the needed width.
+    $lines = explode($eol, $text);
+    array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => drupal_strlen($indent)));
+    $text = implode($eol, $lines);
+  }
+  else {
+    // Wrap this line.
+    _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => drupal_strlen($indent)));
+  }
+  // Empty lines with nothing but spaces.
+  $text = preg_replace('/^ +\r?\n/m', $eol, $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 . drupal_substr(preg_replace('/^/m', $clean_indent, $text), drupal_strlen($indent));
+  return $text;
+}
+
+/**
+ * Transform an HTML string into plain text, preserving the structure of the
+ * markup. Useful for preparing the body of a node to be sent by e-mail.
+ *
+ * The output will be suitable for use as 'format=flowed; delsp=yes' text
+ * (RFC 3676) and can be passed directly to drupal_mail() for sending.
+ *
+ * We deliberately use variable_get('mail_line_endings', MAIL_LINE_ENDINGS)
+ * rather than "\r\n".
+ *
+ * This function provides suitable alternatives for the following tags:
+ *
+ * <a> <address> <b> <blockquote> <br /> <dd> <dl> <dt> <em>
+ * <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <ol> <p> <pre> <strong> <ul>
+ *
+ * The following tags are also handled:
+ *
+ * <del> <div> <ins> <tr>: Rendered the same as a <p> tag.
+ *
+ * <td>: A space is inserted between adjacent table cells.
+ *
+ * @param $string
+ *   The string to be transformed.
+ * @param $allowed_tags
+ *   (optional) If supplied, a list of tags that will be transformed. If
+ *   omitted, all supported tags are transformed.
+ *
+ * @return
+ *   The transformed string.
+ *
+ * @see drupal_mail()
+ */
+function simplenews_html_to_text($string, $allowed_tags = NULL) {
+  $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
+  // Cache list of supported tags.
+  static $supported_tags;
+  if (!isset($supported_tags)) {
+    $supported_tags = array(
+      'a', 'address', 'b', 'blockquote', 'br', 'dd', 'del', 'div', 'dl', 'dt',
+      'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'li', 'ol',
+      'p', 'pre', 'strong', 'td', 'tr', 'ul',
+    );
+  }
+
+  // Make sure only supported tags are kept.
+  $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags;
+
+  // Parse $string into a DOM tree.
+  $dom = filter_dom_load($string);
+  $notes = array();
+  $text = _simplenews_html_to_text($dom->documentElement, $allowed_tags, $notes);
+  // Add footnotes;
+  foreach ($notes as $url => $note) {
+    $text .= $eol . '[' . $note . '] ' . $url;
+  }
+  return trim($text, $eol);
+}
+
+/**
+ * Helper function for simplenews_html_to_text
+ *
+ * Recursively converts $node to text, wrapping and indenting as necessary.
+ *
+ * @param $node
+ *   The source DOMNode.
+ * @param $allowed_tags
+ *   A list of tags that will be transformed.
+ * @param $notes
+ *   The list of footnotes, an associative array of (url => reference number) items.
+ * @param $parents
+ *   The list of ancestor tags, from nearest to most distant.
+ * @param $count
+ *   The number to use for the next list item within an ordered list.
+ */
+function _simplenews_html_to_text(DOMNode $node, array $allowed_tags, array &$notes, $parents = array(), &$count = NULL) {
+  if (is_null($count)) {
+    $count = 1;
+  }
+  $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
+  if ($node->nodeType === XML_TEXT_NODE) {
+    // For text nodes, we just copy the text content.
+    $text = $node->textContent;
+    // Collapse whitespace except within pre tags.
+    if (!in_array('pre', $parents)) {
+      $text = preg_replace('/[[:space:]]+/', ' ', $text);
+    }
+    return $text;
+  }
+  // Non-text node.
+  $tag = '';
+  $text = '';
+  $child_text = '';
+  $child_count = 1;
+  $prefix = '';
+  $indent = '';
+  $suffix = '';
+  if (isset($node->tagName) && in_array($node->tagName, $allowed_tags)) {
+    $tag = $node->tagName;
+    switch ($tag) {
+      // Turn links with valid hrefs into footnotes.
+      case 'a':
+        if ( !empty($node->attributes)
+          && ($href = $node->attributes->getNamedItem('href'))
+          && ($url = $href->nodeValue)
+          && valid_url($url) ) {
+          // Only add links that have not already been added.
+          if (isset($notes[$url])) {
+            $note = $notes[$url];
+          }
+          else {
+            $note = count($notes) + 1;
+            $notes[$url] = $note;
+          }
+          $suffix = ' [' . $note . ']';
+        }
+        break;
+
+      // Generic block-level tags.
+      case 'address':
+      case 'del':
+      case 'div':
+      case 'ins':
+      case 'p':
+      case 'pre':
+        $text = $eol;
+        $suffix = $eol;
+        break;
+
+      // Forced line break.
+      case 'br':
+        $text = $eol;
+        break;
+
+      // Boldface by wrapping with "*" characters.
+      case 'b':
+      case 'strong':
+        $prefix = '*';
+        $suffix = '*';
+        break;
+
+      // Italicize by wrapping with "/" characters.
+      case 'em':
+      case 'i':
+        $prefix = '/';
+        $suffix = '/';
+        break;
+
+      // Blockquotes are indented by "> " at each level.
+      case 'blockquote':
+        $text = $eol;
+        $indent = '> ';
+        $suffix = $eol;
+        break;
+
+      // Dictionary definitions are indented by four spaces.
+      case 'dd':
+        $indent = '    ';
+        $suffix = $eol;
+        break;
+
+      // Dictionary list.
+      case 'dl':
+        // Start on a newline except inside other lists.
+        if (!in_array('li', $parents)) {
+          $text = $eol;
+        }
+
+      // Dictionary term.
+      case 'dt':
+        $suffix = $eol;
+        break;
+
+      // Header level 1 is prefixed by eight "=" characters.
+      case 'h1':
+        $text = $eol;
+        $indent = '======== ';
+        $suffix = $eol;
+        break;
+
+      // Header level 2 is prefixed by six "-" characters.
+      case 'h2':
+        $text = $eol;
+        $indent = '------ ';
+        $suffix = $eol;
+        break;
+
+      // Header level 3 is prefixed by four "." characters and a space.
+      case 'h3':
+        $text = $eol;
+        $indent = '.... ';
+        $suffix = $eol;
+        break;
+
+      // Header level 4 is prefixed by three "." characters and a space.
+      case 'h4':
+        $text = $eol;
+        $indent = '... ';
+        $suffix = $eol;
+        break;
+
+      // Header level 5 is prefixed by two "." character and a space.
+      case 'h5':
+        $text = $eol;
+        $indent = '.. ';
+        $suffix = $eol;
+        break;
+
+      // Header level 6 is prefixed by one "." character and a space.
+      case 'h6':
+        $text = $eol;
+        $indent = '. ';
+        $suffix = $eol;
+        break;
+
+      // Horizontal rulers become a line of 78 "-" characters.
+      case 'hr':
+        $text = $eol . str_repeat('-', 78) . $eol;
+        break;
+
+      // List items are treated differently depending on the parent tag.
+      case 'li':
+        // Ordered list item.
+        if (reset($parents) === 'ol') {
+          // Check the value attribute.
+          if ( !empty($node->attributes)
+            && ($value = $node->attributes->getNamedItem('value'))) {
+            $count = $value->nodeValue;
+          }
+          $indent = " $count) ";
+          $count++;
+        }
+        // Unordered list item.
+        else {
+          $indent = ' * ';
+        }
+        $suffix = $eol;
+        break;
+
+      // Ordered lists.
+      case 'ol':
+        // Start on a newline except inside other lists.
+        if (!in_array('li', $parents)) {
+          $text = $eol;
+        }
+        // Check the start attribute.
+        if ( !empty($node->attributes)
+          && ($value = $node->attributes->getNamedItem('start')) ) {
+          $child_count = $value->nodeValue;
+        }
+        break;
+
+      // Start and end tables on a new line.
+      case 'table':
+        $text = $eol;
+        $suffix = $eol;
+        break;
+
+      // Wrap table cells in space characters.
+      case 'td':
+        $suffix = ' ';
+        break;
+
+      // End each table row with a newline.
+      case 'tr':
+        $suffix = $eol;
+        break;
+
+      // Unordered lists.
+      case 'ul':
+        // Start on a newline except inside other lists.
+        if (!in_array('li', $parents)) {
+          $text = $eol;
+        }
+        break;
+
+      default:
+        break;
+    }
+    // Only add allowed tags to the $parents array.
+    array_unshift($parents, $tag);
+  }
+  // Copy each child node to output.
+  if ($node->hasChildNodes()) {
+    foreach ($node->childNodes as $child) {
+      $child_text .= _simplenews_html_to_text($child, $allowed_tags, $notes, $parents, $child_count);
+    }
+  }
+  // We only add prefix and suffix if the child nodes were non-empty.
+  if ($child_text) {
+    // Don't add a newline to an existing newline.
+    if ($suffix === $eol && drupal_substr($child_text, - drupal_strlen($eol)) === $eol) {
+      $suffix = '';
+    }
+    $child_text = $prefix . $child_text . $suffix;
+    $child_text = simpletest_wrap_mail($child_text, $indent);
+    // We capitalize the contents of h1 and h2 tags.
+    if ($tag === 'h1' || $tag === 'h2') {
+      $child_text = drupal_strtoupper($child_text);
+      // For h1 and h2 tags at the top level, pad each non-empty line with the
+      // character used for indentation.
+      if (count($parents) == 1) {
+        $pad = drupal_substr($indent, 0, 1);
+        $lines = explode($eol, $child_text);
+        foreach ($lines as $i => $line) {
+          if ($line) {
+            $lines[$i] = trim(_simplenews_html_to_text_pad($line . ' ', $pad), $eol);
+          }
+        }
+        $child_text = implode($eol, $lines);
+      }
+    }
+    $text .= $child_text;
+  }
+  return $text;
+}
+
+/**
+ * Helper function for simplenews_html_to_text().
+ *
+ * Pad the last line with the given character.
+ */
+function _simplenews_html_to_text_pad($text, $pad, $prefix = '') {
+  $eol = variable_get('mail_line_endings', MAIL_LINE_ENDINGS);
+  // Remove last line break.
+  $text = preg_replace('/\r?\n$/s', '', $text);
+  // Calculate needed padding space and add it.
+  if (($p = strrpos($text, $eol)) === FALSE) {
+    $p = -1;
+  }
+  else {
+    // Convert position from byte count to character count.
+    $p = drupal_strlen(substr($text, 0, $p));
+  }
+  // subtracting the result of strrpos()
+  $n = max(0, 78 - (drupal_strlen($text) - $p) - drupal_strlen($prefix));
+  // Add prefix and padding, and restore linebreak.
+  return $text . $prefix . str_repeat($pad, $n) . $eol;
+}
diff --git a/includes/simplenews.mail.inc b/includes/simplenews.mail.inc
index 8eaad9b..fa641c2 100644
--- a/includes/simplenews.mail.inc
+++ b/includes/simplenews.mail.inc
@@ -709,7 +709,7 @@ function _simplenews_set_from($category = NULL) {
  * @return string
  *   The target text with HTML and special characters replaced.
  */
-function simplenews_html_to_text($text, $inline_hyperlinks = TRUE) {
+function simplenews_text($text, $inline_hyperlinks = TRUE) {
   // By replacing <a> tag by only its URL the URLs will be placed inline
   // in the email body and are not converted to a numbered reference list
   // by drupal_html_to_text().
@@ -723,8 +723,12 @@ function simplenews_html_to_text($text, $inline_hyperlinks = TRUE) {
   $preg = _simplenews_html_replace();
   $text = preg_replace(array_keys($preg), array_values($preg), $text);
 
-  // Perform standard drupal html to text conversion.
-  return drupal_html_to_text($text);
+  // @todo When #299138 is resolved, delete this workaround.
+  module_load_include('inc', 'simplenews', 'html_to_text');
+  // Convert to text using an improved algorithm.
+  $text = simplenews_html_to_text($text);
+  //@todo End workaround.
+  return $text;
 }
 
 /**
diff --git a/simplenews.info b/simplenews.info
index 37ac6a4..c955582 100644
--- a/simplenews.info
+++ b/simplenews.info
@@ -15,3 +15,4 @@ files[] = includes/views/handlers/simplenews_handler_filter_newsletter_priority.
 files[] = includes/views/handlers/simplenews_handler_filter_category_hyperlinks.inc
 files[] = includes/views/handlers/simplenews_handler_filter_category_new_account.inc
 files[] = includes/views/handlers/simplenews_handler_filter_category_opt_inout.inc
+files[] = simplenews.test
diff --git a/simplenews.module b/simplenews.module
index d2aa0fa..8e34412 100644
--- a/simplenews.module
+++ b/simplenews.module
@@ -2133,7 +2133,7 @@ function simplenews_build_node_mail(&$message, $params) {
   $message['body']['body'] = token_replace($body, $context, array('sanitize' => FALSE));
   if ($category->format == 'plain') {
     module_load_include('inc', 'simplenews', 'includes/simplenews.mail');
-    $message['body']['body'] = simplenews_html_to_text($message['body']['body'], $category->hyperlinks);
+    $message['body']['body'] = simplenews_text($message['body']['body'], $category->hyperlinks);
   }
 
   // Build message footer, replace tokens.
diff --git a/simplenews.test b/simplenews.test
new file mode 100644
index 0000000..e4179cf
--- /dev/null
+++ b/simplenews.test
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * Unit tests for simplenews_html_to_text().
+ */
+class SimplenewsHtmlToTextTestCase extends DrupalUnitTestCase {
+
+  function setUp() {
+    parent::setUp('simplenews');
+    module_load_include('inc', 'simplenews', 'html_to_text');
+  }
+
+  public static function getInfo() {
+    return array(
+      'name'  => 'Simplenews HTML to text conversion',
+      'description' => 'Tests simplenews_html_to_text().',
+      'group' => 'Simplenews',
+    );
+  }
+
+  function _string_to_html($text) {
+    return '"' .
+      str_replace(
+        array("\n", ' '),
+        array('\n', '&nbsp;'),
+        check_plain($text)
+      ) . '"';
+  }
+
+  /**
+   * Test all supported tags of simplenews_html_to_text().
+   */
+  function testTags() {
+    $tests = array(
+      '<a href = "http://drupal.org">Drupal.org</a>' => "Drupal.org [1]\n[1] http://drupal.org",
+      '<address>Drupal</address>' => "Drupal",
+      '<address>Drupal</address><address>Drupal</address>' => "Drupal\n\nDrupal",
+      '<b>Drupal</b>' => "*Drupal*",
+      '<blockquote>Drupal</blockquote>' => " > Drupal",
+      '<blockquote>Drupal</blockquote><blockquote>Drupal</blockquote>' => " > Drupal\n\n > Drupal",
+      '<br />Drupal<br />Drupal<br /><br />Drupal' => "Drupal\nDrupal\n\nDrupal",
+      '<br/>Drupal<br/>Drupal<br/><br/>Drupal' => "Drupal\nDrupal\n\nDrupal",
+      '<br/>Drupal<br/>Drupal<br/><br/>Drupal<p>Drupal</p>' => "Drupal\nDrupal\n\nDrupal\nDrupal",
+      '<del>Drupal</del>' => "Drupal",
+      '<del>Drupal</del><p>Drupal</p>' => "Drupal\n\nDrupal",
+      '<del>Drupal</del><del>Drupal</del>' => "Drupal\n\nDrupal",
+      '<div>Drupal</div>' => "Drupal",
+      '<div>Drupal</div><div>Drupal</div>' => "Drupal\n\nDrupal",
+      '<em>Drupal</em>' => "/Drupal/",
+      '<h1>Drupal</h1>' => "======== DRUPAL " . str_repeat('=', 61),
+      '<h1>Drupal</h1><p>Drupal</p>' => "======== DRUPAL " . str_repeat('=', 61) . "\n\nDrupal",
+      '<h2>Drupal</h2>' => "------ DRUPAL " . str_repeat('-', 63),
+      '<h2>Drupal</h2><p>Drupal</p>' => "------ DRUPAL " . str_repeat('-', 63) . "\n\nDrupal",
+      '<h3>Drupal</h3>' => ".... Drupal",
+      '<h3>Drupal</h3><p>Drupal</p>' => ".... Drupal\n\nDrupal",
+      '<h4>Drupal</h4>' => "... Drupal",
+      '<h4>Drupal</h4><p>Drupal</p>' => "... Drupal\n\nDrupal",
+      '<h5>Drupal</h5>' => ".. Drupal",
+      '<h5>Drupal</h5><p>Drupal</p>' => ".. Drupal\n\nDrupal",
+      '<h6>Drupal</h6>' => ". Drupal",
+      '<h6>Drupal</h6><p>Drupal</p>' => ". Drupal\n\nDrupal",
+      '<hr />Drupal<hr />' => str_repeat('-', 78) . "\nDrupal\n" . str_repeat('-', 78),
+      '<hr/>Drupal<hr/>' => str_repeat('-', 78) . "\nDrupal\n" . str_repeat('-', 78),
+      '<hr/>Drupal<hr/><p>Drupal</p>' => str_repeat('-', 78) . "\nDrupal\n" . str_repeat('-', 78) . "\n\nDrupal",
+      '<ins>Drupal</ins>' => "Drupal",
+      '<i>Drupal</i>' => "/Drupal/",
+      '<p>Drupal</p>' => "Drupal",
+      '<p>Drupal</p><p>Drupal</p>' => "Drupal\n\nDrupal",
+      '<pre>Drupal</pre>' => "Drupal",
+      '<pre>Drupal</pre>Drupal' => "Drupal\nDrupal",
+      '<pre>Drupal</pre><p>Drupal</p>' => "Drupal\n\nDrupal",
+      '<strong>Drupal</strong>' => "*Drupal*",
+      '<table><tr><td>Drupal</td><td>Drupal</td></tr><tr><td>Drupal</td><td>Drupal</td></tr></table>' => "Drupal Drupal\nDrupal Drupal",
+      '<table><tr><td>Drupal</td></tr></table><p>Drupal</p>' => "Drupal\n\nDrupal",
+      '<ul><li>Drupal</li></ul>' => " * Drupal",
+      '<ul><li>Drupal <em>Drupal</em> Drupal</li></ul>' => " * Drupal /Drupal/ Drupal",
+      '<ul><li>Drupal</li><li><ol><li>Drupal</li><li>Drupal</li></ol></li></ul>' => " * Drupal\n *  1) Drupal\n    2) Drupal",
+      '<ul><li>Drupal</li><li><ol><li>Drupal</li></ol></li><li>Drupal</li></ul>' => " * Drupal\n *  1) Drupal\n * Drupal",
+      '<ul><li>Drupal</li><li>Drupal</li></ul>' => " * Drupal\n * Drupal",
+      '<ul><li>Drupal</li></ul><p>Drupal</p>' => " * Drupal\n\nDrupal",
+      '<ol><li>Drupal</li></ol>' => " 1) Drupal",
+      '<ol><li>Drupal</li><li><ul><li>Drupal</li><li>Drupal</li></ul></li></ol>' => " 1) Drupal\n 2)  * Drupal\n     * Drupal",
+      '<ol><li>Drupal</li><li>Drupal</li></ol>' => " 1) Drupal\n 2) Drupal",
+      '<ol>Drupal</ol>' => "Drupal",
+      '<ol><li>Drupal</li></ol><p>Drupal</p>' => " 1) Drupal\n\nDrupal",
+      '<dl><dt>Drupal</dt></dl>' => "Drupal",
+      '<dl><dt>Drupal</dt><dd>Drupal</dd></dl>' => "Drupal\n    Drupal",
+      '<dl><dt>Drupal</dt><dd>Drupal</dd><dt>Drupal</dt><dd>Drupal</dd></dl>' => "Drupal\n    Drupal\nDrupal\n    Drupal",
+      '<dl><dt>Drupal</dt><dd>Drupal</dd></dl><p>Drupal</p>' => "Drupal\n    Drupal\n\nDrupal",
+      '<dl><dt>Drupal<dd>Drupal</dl>' => "Drupal\n    Drupal",
+      '<dl><dt>Drupal</dt></dl><p>Drupal</p>' => "Drupal\n\nDrupal",
+      '<ul><li>Drupal</li><li><dl><dt>Drupal</dt><dd>Drupal</dd><dt>Drupal</dt><dd>Drupal</dd></dl></li><li>Drupal</li></ul>' => " * Drupal\n * Drupal\n       Drupal\n   Drupal\n       Drupal\n * Drupal",
+      // Tests malformed HTML tags.
+      '<br>Drupal<br>Drupal' => "Drupal\nDrupal",
+      '<hr>Drupal<hr>Drupal' => str_repeat('-', 78) . "\nDrupal\n" . str_repeat('-', 78) . "\nDrupal",
+      '<ol><li>Drupal<li>Drupal</ol>' => " 1) Drupal\n 2) Drupal",
+      '<ul><li>Drupal <em>Drupal</em> Drupal</ul></ul>' => " * Drupal /Drupal/ Drupal",
+      '<ul><li>Drupal<li>Drupal</ol>' => " * Drupal\n * Drupal",
+      '<ul><li>Drupal<li>Drupal</ul>' => " * Drupal\n * Drupal",
+      '<ul>Drupal</ul>' => "Drupal",
+      'Drupal</ul></ol></dl><li>Drupal' => "Drupal * Drupal",
+      '<dl>Drupal</dl>' => "Drupal",
+      '<dl>Drupal</dl><p>Drupal</p>' => "Drupal\n\nDrupal",
+      '<dt>Drupal</dt>' => "Drupal",
+      // Tests some unsupported HTML tags.
+      '<html>Drupal</html>' => "Drupal",
+      '<script type="text/javascript">Drupal</script>' => "",
+    );
+
+    foreach ($tests as $html => $text) {
+      $result = simplenews_html_to_text($html);
+      $this->assertEqual($result, $text,
+        'html = ' . $this->_string_to_html($html)
+        . '<br />'
+        . 'result = ' . $this->_string_to_html($result)
+        . '<br />'
+        . 'expected = ' . $this->_string_to_html($text)
+      );
+    }
+  }
+
+  /**
+   * Test $allowed_tags argument of simplenews_html_to_text().
+   */
+  function testSimplenewsHtmlToTextArgs() {
+    // The second parameter of simplenews_html_to_text() overrules the allowed tags.
+    $result = simplenews_html_to_text('Drupal <b>Drupal</b> Drupal', array('b'));
+    $this->assertEqual($result, 'Drupal *Drupal* Drupal', 'Allowed &lt;b&gt; tag found.');
+
+    $result = simplenews_html_to_text('Drupal <h1>Drupal</h1> Drupal', array('b'));
+    $this->assertEqual($result, 'Drupal Drupal Drupal', 'Disallowed &lt;h1&gt; tag not found.');
+
+    $result = simplenews_html_to_text('Drupal <p><em><b>Drupal</b></em><p> Drupal', array('a', 'br', 'h1'));
+    $this->assertEqual($result, 'Drupal Drupal Drupal', 'Disallowed &lt;p&gt;, &lt;em&gt;, and &lt;b&gt; tags not found.');
+
+    $result = simplenews_html_to_text('<html><body>Drupal</body></html>', array('html', 'body'));
+    $this->assertEqual($result, 'Drupal', 'Unsupported &lt;html&gt; and &lt;body&gt; tags not found.');
+  }
+
+  /**
+   * Test that whitespace is collapsed, except within <pre> tags.
+   */
+  function testSimplenewsHtmltoTextCollapsesWhitespace() {
+    $input = "<pre>Drupal  Drupal\n\nDrupal<pre>Drupal  Drupal\n\nDrupal</pre>Drupal  Drupal\n\nDrupal</pre>";
+    $collapsed = "Drupal Drupal DrupalDrupal Drupal DrupalDrupal Drupal Drupal";
+    $preserved = "Drupal  Drupal\n\nDrupal\nDrupal  Drupal\n\nDrupal\nDrupal  Drupal\n\nDrupal";
+    $result = simplenews_html_to_text($input, array('p'));
+    $this->assertEqual($result, $collapsed,
+      'Whitespace inside disallowed &lt;pre&gt; tags is collapsed:<br />'
+      . 'html = ' . $this->_string_to_html($input)
+      . '<br />'
+      . 'result = ' . $this->_string_to_html($result)
+      . '<br />'
+      . 'expected = ' . $this->_string_to_html($collapsed)
+    );
+    $result = simplenews_html_to_text($input);
+    $this->assertEqual($result, $preserved,
+      'Whitespace inside allowed &lt;pre&gt; tags is preserved:<br />'
+      . 'html = ' . $this->_string_to_html($input)
+      . '<br />'
+      . 'result = ' . $this->_string_to_html($result)
+      . '<br />'
+      . 'expected = ' . $this->_string_to_html($preserved)
+    );
+  }
+
+  /**
+   * Test that text separated by block-level tags in HTML get separated by
+   * (at least) a newline in the plaintext version.
+   */
+  function testDrupalHtmlToTextBlockTagToNewline() {
+    $input = '[text]'
+      . '<address>[address]</address>'
+      . '<blockquote>[blockquote]</blockquote>'
+      . '<br />[br]'
+      . '<del>[del]</del>'
+      . '<div>[div]</div>'
+      . '<dl><dt>[dl-dt]</dt>'
+      . '<dt>[dt]</dt>'
+      . '<dd>[dd]</dd>'
+      . '<dd>[dd-dl]</dd></dl>'
+      . '<h1>[h1]</h1>'
+      . '<h2>[h2]</h2>'
+      . '<h3>[h3]</h3>'
+      . '<h4>[h4]</h4>'
+      . '<h5>[h5]</h5>'
+      . '<h6>[h6]</h6>'
+      . '<hr />[hr]'
+      . '<ins>[ins]</ins>'
+      . '<ol><li>[ol-li]</li>'
+      . '<li>[li]</li>'
+      . '<li>[li-ol]</li></ol>'
+      . '<p>[p]</p>'
+      . '<pre>[pre]</pre>'
+      . '<table><thead><tr><td>[table-thead--tr-td]</td></tr></thead>'
+      . '<tbody><tr><td>[tbody-tr-td]</td></tr>'
+      . '<tr><td>[tr-td]</td></tr></tbody></table>'
+      . '<ul><li>[ul-li]</li>'
+      . '<li>[li-ul]</li></ul>'
+      . '[text]';
+    $output = simplenews_html_to_text($input);
+    $this->assertFalse(
+      preg_match('/\][^\n]*\[/s', $output),
+      'Block-level HTML tags should force newlines: '
+      . nl2br(check_plain($output))
+    );
+    $output_upper = drupal_strtoupper($output);
+    $upper_input = drupal_strtoupper($input);
+    $upper_output = simplenews_html_to_text($upper_input);
+    $this->assertEqual(
+      $upper_output,
+      $output_upper,
+      'Tag recognition should be case-insensitive:<br />'
+      . $upper_output
+      . '<br />should  be equal to <br />'
+      . $output_upper
+    );
+  }
+}
