From 2e9519acb50fdb7f594811ee2e43906e33f3d0bb Mon Sep 17 00:00:00 2001 From: Merci Jacob Date: Tue, 23 Jan 2024 17:31:03 +0200 Subject: [PATCH] [ENH] show the list of all external resources in an email and allow the user to approve each one individually or all at once or approve all resources from the sender --- modules/core/message_functions.php | 12 ++- modules/imap/handler_modules.php | 4 +- modules/imap/output_modules.php | 21 ++--- modules/imap/site.css | 4 +- modules/imap/site.js | 138 ++++++++++++++++++++++++++--- modules/ldap_contacts/modules.php | 4 +- 6 files changed, 157 insertions(+), 26 deletions(-) diff --git a/modules/core/message_functions.php b/modules/core/message_functions.php index 5c8ed1c74f..6ebfb11009 100644 --- a/modules/core/message_functions.php +++ b/modules/core/message_functions.php @@ -16,16 +16,26 @@ if (!hm_exists('format_msg_html')) { function format_msg_html($str, $images=false) { $str = str_ireplace('', '', $str); + $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML.DefinitionID', 'hm-message'); + $config->set('HTML.DefinitionRev', 1); $config->set('Cache.DefinitionImpl', null); $config->set('HTML.TargetBlank', true); $config->set('HTML.TargetNoopener', true); - $config->set('HTML.Allowed', 'a[href|target]'); + if (!$images) { $config->set('URI.DisableExternalResources', true); } $config->set('URI.AllowedSchemes', array('mailto' => true, 'data' => true, 'http' => true, 'https' => true)); $config->set('Filter.ExtractStyleBlocks.TidyImpl', true); + + $def = $config->getHTMLDefinition(true); + $html_tags = ['img', 'script', 'iframe', 'audio', 'embed', 'source', 'track', 'video']; + foreach ($html_tags as $tag) { + $def->addAttribute($tag, 'data-src', 'Text'); + } + try { $purifier = new HTMLPurifier($config); return $purifier->purify($str); diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index 6dc9019674..be7b72f8c3 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -1829,9 +1829,7 @@ public function process() { elseif (isset($this->request->post['imap_prefetch']) && $this->request->post['imap_prefetch']) { $prefetch = true; } - if (array_key_exists('imap_allow_images', $this->request->post) && $this->request->post['imap_allow_images']) { - $this->out('imap_allow_images', true); - } + $this->out('header_allow_images', $this->config->get('allow_external_image_sources')); $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']); diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index 2d476ed885..ecc02b0243 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -88,6 +88,7 @@ class Hm_Output_filter_message_body extends Hm_Output_Module { /** * Format html, text, or image content */ + protected function output() { $txt = '
'; if ($this->get('msg_text')) { @@ -97,17 +98,17 @@ protected function output() { } if (isset($struct['subtype']) && strtolower($struct['subtype']) == 'html') { $allowed = $this->get('header_allow_images'); - $images = $this->get('imap_allow_images', false); - if ($allowed && stripos($this->get('msg_text'), 'img')) { - if (!$images) { - $id = $this->get('imap_msg_part'); - $txt .= ''; - } + $msgText = $this->get('msg_text'); + // Everything in the message starting with src="http:// or src="https:// or src='http:// or src='https:// + $externalResRegexp = '/src="(https?:\/\/[^"]*)"|src=\'(https?:\/\/[^\']*)\'/i'; + + if ($allowed) { + $msgText = preg_replace_callback($externalResRegexp, function ($matches) { + return 'data-src="' . $matches[1] . '" ' . 'src="" ' . 'data-message-part="' . $this->html_safe($this->get('imap_msg_part')) . '"'; + }, $msgText); } - $txt .= format_msg_html($this->get('msg_text'), $images); + + $txt .= format_msg_html($msgText, $allowed); } elseif (isset($struct['type']) && strtolower($struct['type']) == 'image') { $txt .= format_msg_image($this->get('msg_text'), strtolower($struct['subtype'])); diff --git a/modules/imap/site.css b/modules/imap/site.css index 60241e9230..40d620e523 100644 --- a/modules/imap/site.css +++ b/modules/imap/site.css @@ -1,6 +1,8 @@ .imap_debug_data { margin-left: 10px; } .imap_connect { display: inline; } -.allow_image_link { margin-right: 20px; float: right; margin-top: -10px; margin-bottom: 10px; } +.notices { display: flex; gap: 20px; align-items: center; margin-bottom: 20px;} +.mobile .notices { flex-direction: column; align-items: flex-start; } +.external_notices { display: flex; gap: 20px; flex-wrap: wrap; } .imap_debug { border: solid 1px #aaa; float: left; padding: 10px; height: 300px; width: 300px; overflow: scroll; white-space: pre; margin-top: 50px; font-size: 75%; } .server_link { line-height: 10pt; margin: 2px; margin-left: 5px; display: block; padding: 5px; border: solid 1px #ddd; background-color: #fff; float: right; clear: none; color: #333; text-decoration: none; border-radius: 3px; } .hl { padding-right: 5px; color: #666; } diff --git a/modules/imap/site.js b/modules/imap/site.js index e512e20405..22f7662a1e 100644 --- a/modules/imap/site.js +++ b/modules/imap/site.js @@ -556,10 +556,7 @@ var expand_imap_folders = function(path) { return false; }; -var get_message_content = function(msg_part, uid, list_path, detail, callback, noupdate, images) { - if (!images) { - images = 0; - } +var get_message_content = function(msg_part, uid, list_path, detail, callback, noupdate) { if (!uid) { uid = $('.msg_uid').val(); } @@ -574,7 +571,6 @@ var get_message_content = function(msg_part, uid, list_path, detail, callback, n [{'name': 'hm_ajax_hook', 'value': 'ajax_imap_message_content'}, {'name': 'imap_msg_uid', 'value': uid}, {'name': 'imap_msg_part', 'value': msg_part}, - {'name': 'imap_allow_images', 'value': images}, {'name': 'imap_server_id', 'value': detail.server_id}, {'name': 'folder', 'value': detail.folder}], function(res) { @@ -712,11 +708,6 @@ var imap_message_view_finished = function(msg_uid, detail, skip_links) { } $('.all_headers').on("click", function() { return Hm_Utils.toggle_long_headers(); }); $('.small_headers').on("click", function() { return Hm_Utils.toggle_long_headers(); }); - $('.msg_part_link').on("click", function() { - $('.header_subject')[0].scrollIntoView(); - $('.msg_text_inner').css('visibility', 'hidden'); - return get_message_content($(this).data('messagePart'), false, false, false, false, false, $(this).data('allowImages')); - }); $('#flag_msg').on("click", function() { return imap_flag_message($(this).data('state')); }); $('#unflag_msg').on("click", function() { return imap_flag_message($(this).data('state')); }); $('#delete_message').on("click", function() { return imap_delete_message(); }); @@ -1257,3 +1248,130 @@ var imap_archive_message = function(state, supplied_uid, supplied_detail) { return false; }; +var imap_show_add_contact_popup = function() { + var popup = document.getElementById("contact_popup"); + popup.classList.toggle("show"); +}; + +var imap_hide_add_contact_popup = function(event) { + event.stopPropagation() + var popup = document.getElementById("contact_popup"); + popup.classList.toggle("show"); +}; + +/** + * Allow external resources for the provided element. + * + * @param {HTMLElement} element - The element containing the allow button. + * @param {string} messagePart - The message part associated with the resource. + * @returns {void} + */ +function handleAllowResource(element, messagePart) { + element.querySelector('a').addEventListener('click', function (e) { + e.preventDefault(); + $('.msg_text_inner').remove(); + const externalSources = $(this).data('src').split(','); + externalSources?.forEach((source) => Hm_Utils.save_to_local_storage(source, 1)); + return get_message_content(messagePart, false, false, false, false, false); + }); +} + +/** + * Create and insert in the DOM an element containing a message and a button to allow the resource. + * + * @param {HTMLElement} element - The element having the blocked resource. + * @returns {void} + */ +function handleInvisibleResource(element) { + const dataSrc = element.dataset.src; + + const allowResource = document.createElement('div'); + // allowResource.classList.add('allow_image_link'); + allowResource.classList.add('alert', 'alert-warning', 'p-1'); + + const source = dataSrc.substring(0, 40) + (dataSrc.length > 40 ? '...' : ''); + allowResource.innerHTML = `Source blocked: ${element.alt ? element.alt : source} + + Allow
+ `; + + document.querySelector('.external_notices').insertAdjacentElement('beforeend', allowResource); + handleAllowResource(allowResource, element.dataset.messagePart); +} + +const mutation = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach(function (node) { + if (node.classList.contains('msg_text_inner')) { + + // Extarnal resources notice boxes container + document.querySelector('.msg_text_inner').insertAdjacentHTML('afterbegin', '
'); + + const sender = document.querySelector('#contact_info').textContent.trim().replace(/\s/g, '_') + 'external_resources_allowed'; + const elements = node.querySelectorAll('[data-src]'); + const blockedResources = []; + elements.forEach(function (element) { + + const dataSrc = element.dataset.src; + const senderAllowed = Hm_Utils.get_from_local_storage(sender); + const allowed = Hm_Utils.get_from_local_storage(dataSrc); + + switch (Number(allowed) || Number(senderAllowed)) { + case 1: + element.src = dataSrc; + break; + default: + if ((allowed || senderAllowed) === null) { + Hm_Utils.save_to_local_storage(dataSrc, 0); + } + handleInvisibleResource(element); + blockedResources.push(dataSrc); + break; + } + }); + + const noticesElement = document.createElement('div'); + noticesElement.classList.add('notices'); + + if(blockedResources.length) { + const allowAll = document.createElement('div'); + allowAll.classList.add('allow_image_link', 'all', 'fw-bold'); + allowAll.textContent = 'For security reasons, external resources have been blocked.'; + if (blockedResources.length > 1) { + const allowAllLink = document.createElement('a'); + allowAllLink.classList.add('btn', 'btn-light', 'btn-sm'); + allowAllLink.href = '#'; + allowAllLink.dataset.src = blockedResources.join(','); + allowAllLink.textContent = 'Allow all'; + allowAll.appendChild(allowAllLink); + handleAllowResource(allowAll, elements[0].dataset.messagePart); + } + noticesElement.appendChild(allowAll); + + const button = document.createElement('a'); + button.classList.add('always_allow_image', 'btn', 'btn-light', 'btn-sm'); + button.textContent = 'Always allow from this sender'; + noticesElement.appendChild(button); + + button.addEventListener('click', function (e) { + e.preventDefault(); + Hm_Utils.save_to_local_storage(sender, 1); + $('.msg_text_inner').remove(); + get_message_content(elements[0].dataset.messagePart, false, false, false, false, false) + }); + } + + document.querySelector('.external_notices').insertAdjacentElement('beforebegin', noticesElement); + } + }); + } + }); +}); + +const message = document.querySelector('.msg_text'); +if (message) { + mutation.observe(document.querySelector('.msg_text'), { + childList: true + }); +} diff --git a/modules/ldap_contacts/modules.php b/modules/ldap_contacts/modules.php index 77cb1d346d..c4bdc5edbe 100644 --- a/modules/ldap_contacts/modules.php +++ b/modules/ldap_contacts/modules.php @@ -796,7 +796,9 @@ function fetch_ldap_contacts($config, $user_config, $contact_store, $session=fal $ldap_config = ldap_add_user_auth($ldap_config, $user_config->get('ldap_contacts_auth_setting', array())); if (count($ldap_config) > 0) { foreach ($ldap_config as $name => $vals) { - $vals['name'] = $name; + if (is_array($vals)) { + $vals['name'] = $name; + } $ldap = new Hm_LDAP_Contacts($vals); if ($ldap->connect()) { $contacts = $ldap->fetch();