Skip to content

Commit

Permalink
[ENH] show the list of all external resources in an email and allow t…
Browse files Browse the repository at this point in the history
…he user to approve each one individually or all at once or approve all resources from the sender
  • Loading branch information
mercihabam committed Feb 21, 2024
1 parent 46f052c commit 2e9519a
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 26 deletions.
12 changes: 11 additions & 1 deletion modules/core/message_functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@
if (!hm_exists('format_msg_html')) {
function format_msg_html($str, $images=false) {
$str = str_ireplace('</body>', '', $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);
Expand Down
4 changes: 1 addition & 3 deletions modules/imap/handler_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
21 changes: 11 additions & 10 deletions modules/imap/output_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Hm_Output_filter_message_body extends Hm_Output_Module {
/**
* Format html, text, or image content
*/

protected function output() {
$txt = '<div class="msg_text_inner">';
if ($this->get('msg_text')) {
Expand All @@ -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 .= '<div class="allow_image_link">'.
'<a href="#" class="msg_part_link" data-allow-images="1" '.
'data-message-part="'.$this->html_safe($id).'">'.
$this->trans('Allow Images').'</a></div>';
}
$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']));
Expand Down
4 changes: 3 additions & 1 deletion modules/imap/site.css
Original file line number Diff line number Diff line change
@@ -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; }
Expand Down
138 changes: 128 additions & 10 deletions modules/imap/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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(); });
Expand Down Expand Up @@ -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}
<a href="#" data-src="${dataSrc}" class="btn btn-light btn-sm">
Allow</a></div>
`;

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', '<div class="external_notices"></div>');

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
});
}
4 changes: 3 additions & 1 deletion modules/ldap_contacts/modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 2e9519a

Please sign in to comment.