Browse Source

Fix XSS vulnerability in post-processing of sanitized HTML content [CVE-2024-42009]

Credits to Oskar Zeino-Mahmalat (https://www.sonarsource.com)
pull/9596/head
Aleksander Machniak 12 months ago
parent
commit
1b3bb11d4f
  1. 4
      CHANGELOG.md
  2. 36
      program/actions/mail/compose.php
  3. 195
      program/actions/mail/index.php
  4. 4
      program/actions/mail/show.php
  5. 9
      program/actions/settings/identity_save.php
  6. 50
      program/lib/Roundcube/rcube_washtml.php
  7. 91
      tests/Actions/Mail/Index.php

4
CHANGELOG.md

@ -2,6 +2,10 @@
## Unreleased
- Fix XSS vulnerability in post-processing of sanitized HTML content [CVE-2024-42009]
## Release 1.5.7
- Enigma: Fix finding of a private key when decrypting a message using GnuPG v2.3
- Fix TinyMCE localization installation (#9266)
- Makefile: Use phpDocumentor v3.4 for the Framework docs (#9313)

36
program/actions/mail/compose.php

@ -396,7 +396,7 @@ class rcmail_action_mail_compose extends rcmail_action_mail_index
// clean HTML message body which can be submitted by URL
if (!empty($COMPOSE['param']['body'])) {
if ($COMPOSE['param']['html'] = strpos($COMPOSE['param']['body'], '<') !== false) {
$wash_params = ['safe' => false, 'inline_html' => true];
$wash_params = ['safe' => false];
$COMPOSE['param']['body'] = self::prepare_html_body($COMPOSE['param']['body'], $wash_params);
}
}
@ -973,39 +973,25 @@ class rcmail_action_mail_compose extends rcmail_action_mail_index
static $part_no;
// Set attributes of the part container
$container_id = self::$COMPOSE['mode'] . 'body' . (++$part_no);
$container_attrib = ['id' => $container_id];
$body_args = [
'safe' => self::$MESSAGE->is_safe,
'plain' => false,
'css_prefix' => 'v' . $part_no,
$container_id = self::$COMPOSE['mode'] . 'body' . (++$part_no);
$wash_params += [
'safe' => self::$MESSAGE->is_safe,
'css_prefix' => 'v' . $part_no,
'add_comments' => false,
];
// remove comments (produced by washtml)
$replace = ['/<!--[^>]+-->/' => ''];
if (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT) {
// convert TinyMCE's empty-line sequence (#1490463)
$replace['/<p>\xC2\xA0<\/p>/'] = '<p><br /></p>';
// remove <body> tags
$replace['/<body([^>]*)>/i'] = '';
$replace['/<\/body>/i'] = '';
$body = preg_replace('/<p>\xC2\xA0<\/p>/', '<p><br /></p>', $body);
// remove <body> tags (not their content)
$wash_params['ignore_elements'] = ['body'];
}
else {
$body_args['container_id'] = $container_id;
$body_args['container_attrib'] = $container_attrib;
$wash_params['container_id'] = $container_id;
}
// Make the HTML content safe and clean
$body = self::wash_html($body, $wash_params + $body_args, self::$CID_MAP);
$body = preg_replace(array_keys($replace), array_values($replace), $body);
$body = self::html4inline($body, $body_args);
if (self::$COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT) {
$body = html::div($container_attrib, $body);
}
return $body;
return self::wash_html($body, $wash_params, self::$CID_MAP);
}
// Removes signature from the message body

195
program/actions/mail/index.php

@ -35,6 +35,7 @@ class rcmail_action_mail_index extends rcmail_action
protected static $PRINT_MODE = false;
protected static $REMOTE_OBJECTS;
protected static $SUSPICIOUS_EMAIL = false;
protected static $wash_html_body_attrs = [];
/**
* Request handler.
@ -931,13 +932,17 @@ class rcmail_action_mail_index extends rcmail_action
// clean HTML with washtml by Frederic Motte
$wash_opts = [
'show_washed' => false,
'add_comments' => $p['add_comments'] ?? true,
'allow_remote' => $p['safe'],
'blocked_src' => $rcmail->output->asset_url('program/resources/blocked.gif'),
'charset' => RCUBE_CHARSET,
'cid_map' => $cid_replaces,
'html_elements' => ['body'],
'css_prefix' => $p['css_prefix'],
'ignore_elements' => $p['ignore_elements'] ?? [],
// internal configuration
'container_id' => $p['container_id'],
'body_class' => $p['body_class'] ?? '',
];
if (empty($p['inline_html'])) {
@ -958,11 +963,25 @@ class rcmail_action_mail_index extends rcmail_action
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
self::$wash_html_body_attrs = [];
if (!empty($p['inline_html'])) {
$washer->add_callback('body', 'rcmail_action_mail_index::washtml_callback');
if ($wash_opts['body_class']) {
self::$wash_html_body_attrs['class'] = $wash_opts['body_class'];
}
if ($wash_opts['container_id']) {
self::$wash_html_body_attrs['id'] = $wash_opts['container_id'];
}
}
if (empty($p['skip_washer_form_callback'])) {
$washer->add_callback('form', 'rcmail_action_mail_index::washtml_callback');
}
// allow CSS styles, will be sanitized by rcmail_washtml_callback()
// allow CSS styles, will be sanitized by self::washtml_callback()
if (empty($p['skip_washer_style_callback'])) {
$washer->add_callback('style', 'rcmail_action_mail_index::washtml_callback');
}
@ -980,6 +999,11 @@ class rcmail_action_mail_index extends rcmail_action
$html = $washer->wash($html);
self::$REMOTE_OBJECTS = $washer->extlinks;
// There was no <body>, but a wrapper element is required
if (!empty($p['inline_html']) && !empty(self::$wash_html_body_attrs)) {
$html = html::tag('div', self::$wash_html_body_attrs, $html);
}
return $html;
}
@ -1109,9 +1133,64 @@ class rcmail_action_mail_index extends rcmail_action
$washtml->extlinks = true;
}
else {
$out = html::tag('style', ['type' => 'text/css'], $decoded);
$out = $decoded;
}
}
if (strlen($out)) {
$css_prefix = $washtml->get_config('css_prefix');
$is_safe = $washtml->get_config('allow_remote');
$body_class = $washtml->get_config('body_class') ?: '';
$cont_id = $washtml->get_config('container_id') ?: '';
$cont_id = trim($cont_id . ($body_class ? " div.{$body_class}" : ''));
$out = rcube_utils::mod_css_styles($out, $cont_id, $is_safe, $css_prefix);
$out = html::tag('style', ['type' => 'text/css'], $out);
}
break;
case 'body':
$style = [];
$attrs = self::$wash_html_body_attrs;
foreach (html::parse_attrib_string($attrib) as $attr_name => $value) {
switch (strtolower($attr_name)) {
case 'bgcolor':
// Get bgcolor, we'll set it as background-color of the message container
if (preg_match('/^([a-z0-9#]+)$/i', $value, $m)) {
$style['background-color'] = $value;
}
break;
case 'text':
// Get text color, we'll set it as font color of the message container
if (preg_match('/^([a-z0-9#]+)$/i', $value, $m)) {
$style['color'] = $value;
}
break;
case 'background':
// Get background, we'll set it as background-image of the message container
if (preg_match('/^([^\s]+)$/', $value, $m)) {
$style['background-image'] = "url({$value})";
}
break;
default:
$attrs[$attr_name] = $value;
}
}
if (!empty($style)) {
foreach ($style as $idx => $val) {
$style[$idx] = $idx . ': ' . $val;
}
$attrs['style'] = ($attrs['style'] ? trim($attrs['style'], ';') . '; ' : '') . implode('; ', $style);
}
$out = html::tag('div', $attrs, $content);
self::$wash_html_body_attrs = [];
break;
}
return $out;
@ -1168,118 +1247,6 @@ class rcmail_action_mail_index extends rcmail_action
}
}
/**
* Modify a HTML message that it can be displayed inside a HTML page
*/
public static function html4inline($body, &$args)
{
$last_pos = 0;
$is_safe = !empty($args['safe']);
$prefix = isset($args['css_prefix']) ? $args['css_prefix'] : null;
$cont_id = trim(
(!empty($args['container_id']) ? $args['container_id'] : '')
. (!empty($args['body_class']) ? ' div.' . $args['body_class'] : '')
);
// find STYLE tags
while (($pos = stripos($body, '<style', $last_pos)) !== false && ($pos2 = stripos($body, '</style>', $pos+1))) {
$pos = strpos($body, '>', $pos) + 1;
$len = $pos2 - $pos;
// replace all css definitions with #container [def]
$styles = substr($body, $pos, $len);
$styles = rcube_utils::mod_css_styles($styles, $cont_id, $is_safe, $prefix);
$body = substr_replace($body, $styles, $pos, $len);
$last_pos = $pos2 + strlen($styles) - $len;
}
$replace = [
// add comments around html and other tags
'/(<!DOCTYPE[^>]*>)/i' => '<!--\\1-->',
'/(<\?xml[^>]*>)/i' => '<!--\\1-->',
'/(<\/?html[^>]*>)/i' => '<!--\\1-->',
'/(<\/?head[^>]*>)/i' => '<!--\\1-->',
'/(<title[^>]*>.*<\/title>)/Ui' => '<!--\\1-->',
'/(<\/?meta[^>]*>)/i' => '<!--\\1-->',
// quote <? of php and xml files that are specified as text/html
'/<\?/' => '&lt;?',
'/\?>/' => '?&gt;',
];
$regexp = '/<body([^>]*)/';
// Handle body attributes that doesn't play nicely with div elements
if (preg_match($regexp, $body, $m)) {
$style = [];
$attrs = $m[0];
// Get bgcolor, we'll set it as background-color of the message container
if (!empty($m[1]) && preg_match('/bgcolor=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) {
$style['background-color'] = $mb[1];
$attrs = preg_replace('/\s?bgcolor=["\']*[a-z0-9#]+["\']*/i', '', $attrs);
}
// Get text color, we'll set it as font color of the message container
if (!empty($m[1]) && preg_match('/text=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) {
$style['color'] = $mb[1];
$attrs = preg_replace('/\s?text=["\']*[a-z0-9#]+["\']*/i', '', $attrs);
}
// Get background, we'll set it as background-image of the message container
if (!empty($m[1]) && preg_match('/background=["\']*([^"\'>\s]+)["\']*/', $attrs, $mb)) {
$style['background-image'] = 'url('.$mb[1].')';
$attrs = preg_replace('/\s?background=["\']*([^"\'>\s]+)["\']*/', '', $attrs);
}
if (!empty($style)) {
$body = preg_replace($regexp, rtrim($attrs), $body, 1);
}
// handle body styles related to background image
if (!empty($style['background-image'])) {
// get body style
if (preg_match('/#'.preg_quote($cont_id, '/').'\s+\{([^}]+)}/i', $body, $m)) {
// get background related style
$regexp = '/(background-position|background-repeat)\s*:\s*([^;]+);/i';
if (preg_match_all($regexp, $m[1], $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$style[$m[1]] = $m[2];
}
}
}
}
if (!empty($style)) {
foreach ($style as $idx => $val) {
$style[$idx] = $idx . ': ' . $val;
}
$args['container_attrib']['style'] = implode('; ', $style);
}
// replace <body> with <div>
if (!empty($args['body_class'])) {
$replace['/<body([^>]*)>/i'] = '<div class="' . $args['body_class'] . '"\\1>';
}
else {
$replace['/<body/i'] = '<div';
}
$replace['/<\/body>/i'] = '</div>';
}
// make sure there's 'rcmBody' div, we need it for proper css modification
// its name is hardcoded in self::message_body() also
else if (!empty($args['body_class'])) {
$body = '<div class="' . $args['body_class'] . '">' . $body . '</div>';
}
// Clean up, and replace <body> with <div>
$body = preg_replace(array_keys($replace), array_values($replace), $body);
return $body;
}
/**
* Parse link (a, link, area) attributes and set correct target
*/

4
program/actions/mail/show.php

@ -724,10 +724,6 @@ class rcmail_action_mail_show extends rcmail_action_mail_index
$rcmail->output->set_env('is_pgp_content', '#' . $container_id);
}
if ($part->ctype_secondary == 'html') {
$body = self::html4inline($body, $body_args);
}
$out .= html::div($body_args['container_attrib'], $plugin['prefix'] . $body);
}
}

9
program/actions/settings/identity_save.php

@ -256,6 +256,8 @@ class rcmail_action_settings_identity_save extends rcmail_action
'charset' => RCUBE_CHARSET,
'html_elements' => ['body', 'link'],
'html_attribs' => ['rel', 'type'],
'ignore_elements' => ['body'],
'add_comments' => false,
];
// initialize HTML washer
@ -264,11 +266,6 @@ class rcmail_action_settings_identity_save extends rcmail_action
// Remove non-UTF8 characters (#1487813)
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
// remove unwanted comments and tags (produced by washtml)
$html = preg_replace(['/<!--[^>]+-->/', '/<\/?body>/'], '', $html);
return $html;
return $washer->wash($html);
}
}

50
program/lib/Roundcube/rcube_washtml.php

@ -164,7 +164,14 @@ class rcube_washtml
public $extlinks = false;
/** @var array Current settings */
private $config = [];
private $config = [
'add_comments' => true,
'allow_remote' => false,
'base_url' => '',
'charset' => RCUBE_CHARSET,
'cid_map' => [],
'show_washed' => true,
];
/** @var array Registered callback functions for tags */
private $handlers = [];
@ -225,11 +232,7 @@ class rcube_washtml
unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements'], $p['css_prefix']);
$this->config = $p + ['show_washed' => true, 'allow_remote' => false, 'cid_map' => [], 'base_url' => ''];
if (!isset($this->config['charset'])) {
$this->config['charset'] = RCUBE_CHARSET;
}
$this->config = array_merge($this->config, $p);
}
/**
@ -576,7 +579,7 @@ class rcube_washtml
);
}
return '<!-- ignored -->';
return $this->config['add_comments'] ? '<!-- ignored -->' : '';
}
$node = $node->firstChild;
@ -590,7 +593,9 @@ class rcube_washtml
if ($tagName == 'link') {
$uri = $this->wash_uri($node->getAttribute('href'), false, false);
if (!$uri) {
$dump .= '<!-- link ignored -->';
if ($this->config['add_comments']) {
$dump .= '<!-- link ignored -->';
}
break;
}
@ -600,7 +605,9 @@ class rcube_washtml
&& self::attribute_value($node, 'attributename', 'href')
) {
// Insecure svg tags
$dump .= "<!-- $tagName blocked -->";
if ($this->config['add_comments']) {
$dump .= "<!-- {$tagName} blocked -->";
}
break;
}
@ -611,13 +618,13 @@ class rcube_washtml
}
else if (isset($this->_html_elements[$tagName])) {
$content = $this->dumpHtml($node, $level);
$dump .= '<' . $node->nodeName;
$tag = '<' . $node->nodeName;
if ($tagName == 'svg') {
$xpath = new DOMXPath($node->ownerDocument);
foreach ($xpath->query('namespace::*') as $ns) {
if ($ns->nodeName != 'xmlns:xml') {
$dump .= sprintf(' %s="%s"',
$tag .= sprintf(' %s="%s"',
$ns->nodeName,
htmlspecialchars($ns->nodeValue, ENT_QUOTES, $this->config['charset'])
);
@ -628,20 +635,25 @@ class rcube_washtml
$content = htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, $this->config['charset']);
}
$dump .= $this->wash_attribs($node);
$tag .= $this->wash_attribs($node);
if ($content === '' && ($this->is_xml || isset($this->_void_elements[$tagName]))) {
$dump .= ' />';
}
else {
$dump .= '>' . $content . '</' . $node->nodeName . '>';
if (isset($this->_ignore_elements[$tagName])) {
$dump .= $content;
} elseif ($content === '' && ($this->is_xml || isset($this->_void_elements[$tagName]))) {
$dump .= $tag . ' />';
} else {
$dump .= $tag . '>' . $content . '</' . $node->nodeName . '>';
}
}
else if (isset($this->_ignore_elements[$tagName])) {
$dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' not allowed -->';
if ($this->config['add_comments']) {
$dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' not allowed -->';
}
}
else {
$dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' ignored -->';
if ($this->config['add_comments']) {
$dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' ignored -->';
}
$dump .= $this->dumpHtml($node, $level); // ignore tags not its content
}
break;

91
tests/Actions/Mail/Index.php

@ -348,25 +348,24 @@ class Actions_Mail_Index extends ActionTestCase
$part = $this->get_html_part('src/htmlbody.txt');
$part->replaces = ['ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg'];
$params = ['container_id' => 'foo'];
$params = ['container_id' => 'foo', 'safe' => false];
// render HTML in normal mode
$body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => false]);
$html = rcmail_action_mail_index::html4inline($body, $params);
$this->assertRegExp('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image");
$this->assertRegExp('#background="program/resources/blocked.gif"#', $html, "Replace external background image");
$this->assertNotRegExp('/ex3.jpg/', $html, "No references to external images");
$this->assertNotRegExp('/<meta [^>]+>/', $html, "No meta tags allowed");
$this->assertNotRegExp('/<form [^>]+>/', $html, "No form tags allowed");
$this->assertRegExp('/Subscription form/', $html, "Include <form> contents");
$this->assertRegExp('/<!-- link ignored -->/', $html, "No external links allowed");
$this->assertRegExp('/<a[^>]+ target="_blank"/', $html, "Set target to _blank");
$html = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$this->assertMatchesRegularExpression('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image");
$this->assertMatchesRegularExpression('#background="program/resources/blocked.gif"#', $html, "Replace external background image");
$this->assertDoesNotMatchRegularExpression('/ex3.jpg/', $html, "No references to external images");
$this->assertDoesNotMatchRegularExpression('/<meta [^>]+>/', $html, "No meta tags allowed");
$this->assertDoesNotMatchRegularExpression('/<form [^>]+>/', $html, "No form tags allowed");
$this->assertMatchesRegularExpression('/Subscription form/', $html, "Include <form> contents");
$this->assertMatchesRegularExpression('/<!-- link ignored -->/', $html, "No external links allowed");
$this->assertMatchesRegularExpression('/<a[^>]+ target="_blank"/', $html, "Set target to _blank");
// $this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected");
// render HTML in safe mode
$body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => true]);
$html = rcmail_action_mail_index::html4inline($body, $params);
$params['safe'] = true;
$html = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$this->assertRegExp('/<style [^>]+>/', $html, "Allow styles in safe mode");
$this->assertRegExp('#src="http://evilsite.net/mailings/ex3.jpg"#', $html, "Allow external images in HTML (safe mode)");
@ -383,17 +382,14 @@ class Actions_Mail_Index extends ActionTestCase
$this->initOutput(rcmail_action::MODE_HTTP, 'mail', '');
$part = $this->get_html_part('src/htmlxss.txt');
$washed = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => true]);
$this->assertNotRegExp('/src="skins/', $washed, "Remove local references");
$this->assertNotRegExp('/\son[a-z]+/', $washed, "Remove on* attributes");
$this->assertNotContains('onload', $washed, "Handle invalid style");
$params = ['container_id' => 'foo', 'safe' => true];
$html = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$params = ['container_id' => 'foo'];
$html = rcmail_action_mail_index::html4inline($washed, $params);
$this->assertNotRegExp('/onclick="return rcmail.command(\'compose\',\'xss@somehost.net\',this)"/', $html, "Clean mailto links");
$this->assertNotRegExp('/alert/', $html, "Remove alerts");
$this->assertDoesNotMatchRegularExpression('/src="skins/', $html, 'Remove local references');
$this->assertDoesNotMatchRegularExpression('/\son[a-z]+/', $html, 'Remove on* attributes');
$this->assertStringNotContainsString('onload', $html, 'Handle invalid style');
$this->assertDoesNotMatchRegularExpression('/onclick="return rcmail.command(\'compose\',\'xss@somehost.net\',this)"/', $html, "Clean mailto links");
$this->assertDoesNotMatchRegularExpression('/alert/', $html, "Remove alerts");
}
/**
@ -406,8 +402,7 @@ class Actions_Mail_Index extends ActionTestCase
$part = $this->get_html_part('src/BID-26800.txt');
$params = ['container_id' => 'dabody', 'safe' => true];
$body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => true]);
$washed = rcmail_action_mail_index::html4inline($body, $params);
$washed = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$this->assertNotRegExp('/alert|expression|javascript|xss/', $washed, "Remove evil style blocks");
$this->assertNotRegExp('/font-style:italic/', $washed, "Allow valid styles");
@ -429,20 +424,40 @@ class Actions_Mail_Index extends ActionTestCase
$this->assertNotRegExp('/vbscript:/', $washed, "Remove vbscript: links");
}
/**
* Test that HTML sanitization does not change attribute (evil) values
*/
public function test_html_body_attributes()
{
$part = $this->get_html_part();
$part->body = '<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=alert(origin) foo=bar">Foo</body>';
$params = ['safe' => true, 'add_comments' => false];
$washed = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$this->assertSame(str_replace('body', 'div', $part->body), $washed);
$params['inline_html'] = false;
$washed = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$this->assertSame('<html><head></head>' . $part->body . '</html>', $washed);
}
/**
* Test handling of body style attributes
*/
function test_html4inline_body_style()
public function test_wash_html_body_style()
{
$html = '<body background="test" bgcolor="#fff" style="font-size:11px" text="#000"><p>test</p></body>';
$params = ['container_id' => 'foo'];
$html = rcmail_action_mail_index::html4inline($html, $params);
$html = '<body background="http://test.com/image" bgcolor="#fff" style="font-size: 11px" text="#000"><p>test</p></body>';
$params = ['container_id' => 'foo', 'add_comments' => false, 'safe' => false];
$washed = \rcmail_action_mail_index::wash_html($html, $params, []);
$this->assertSame('<div id="foo" style="font-size: 11px; background-image: url(program/resources/blocked.gif); background-color: #fff; color: #000"><p>test</p></div>', $washed);
$params['safe'] = true;
$washed = \rcmail_action_mail_index::wash_html($html, $params, []);
$this->assertRegExp('/<div style="font-size:11px">/', $html, "Body attributes");
$this->assertArrayHasKey('container_attrib', $params, "'container_attrib' param set");
$this->assertRegExp('/background-color: #fff;/', $params['container_attrib']['style'], "Body style (bgcolor)");
$this->assertRegExp('/background-image: url\(test\)/', $params['container_attrib']['style'], "Body style (background)");
$this->assertRegExp('/color: #000/', $params['container_attrib']['style'], "Body style (text)");
$this->assertSame('<div id="foo" style="font-size: 11px; background-image: url(http://test.com/image); background-color: #fff; color: #000"><p>test</p></div>', $washed);
}
/**
@ -468,6 +483,7 @@ class Actions_Mail_Index extends ActionTestCase
$meta = '<meta charset="'.RCUBE_CHARSET.'" />';
$args = [
'inline_html' => false,
'html_elements' => ['html', 'body', 'meta', 'head'],
'html_attribs' => ['charset'],
];
@ -524,11 +540,10 @@ class Actions_Mail_Index extends ActionTestCase
$this->initOutput(rcmail_action::MODE_HTTP, 'mail', '');
$part = $this->get_html_part('src/mailto.txt');
$params = ['container_id' => 'foo'];
$params = ['container_id' => 'foo', 'safe' => false];
// render HTML in normal mode
$body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => false]);
$html = rcmail_action_mail_index::html4inline($body, $params);
$html = \rcmail_action_mail_index::print_body($part->body, $part, $params);
$mailto = '<a href="mailto:me@me.com"'
.' onclick="return rcmail.command(\'compose\',\'me@me.com?subject=this is the subject&amp;body=this is the body\',this)" rel="noreferrer">e-mail</a>';

Loading…
Cancel
Save