File: //usr/share/webmin/authentic-theme/extensions/mail/mail.src.js
/*!
* Authentic Theme (https://github.com/authentic-theme/authentic-theme)
* Copyright Ilia Rostovtsev <programming@rostovtsev.io>
* Licensed under MIT (https://github.com/authentic-theme/authentic-theme/blob/master/LICENSE)
*/
/* jshint strict: true */
/* jshint esversion: 6 */
/* jslint bitwise: true */
/* jshint jquery: true */
'use strict';
/**
* Mail object module
*
* @since 19.17
*
* @return {object} Reveal folders module API
* @return {object} Reveal messages module API
*/
const mail = (function() {
/* jshint -W117 */
/**
* Imports globals using abstraction layer (DI)
*
* @since 19.17
*
* @return {string|function|object}
*/
const _ = {
path: {
origin: v___location_origin,
prefix: v___location_prefix,
extensions: v___server_extensions_path,
css: v___server_css_path,
js: v___server_js_path
},
variable: {
switch: function() {
return $t_uri_webmail
},
module: {
name: function() {
return v___module
},
link: function() {
let prefix = v___location_prefix;
return prefix ? `${prefix}/${v___module}` : `/${this.name()}`;
},
},
},
content: get_pjax_content,
load: load,
language: theme_language,
notification: plugins.messenger.post,
rows: page_table_rows_control,
document_title: theme_title_generate,
navigation: {
reset: plugins.navigation.reset
},
plugin: {
json_to_query: Convert.json_to_query,
timestamp: snippets.datetime.locale,
offset_adjust: page.handle.content.offset,
select: (data, size = '34') => {
if (Array.isArray(data)) {
data[0].select2(data[1])
return
}
data.select2({
minimumResultsForSearch: 5,
containerCssClass: `select2-content heighter-${size}`,
dropdownCssClass: `select2-content h${size}`
});
data.next('.select2').addClass('select2-content-container')
data.on('select2:open', function() {
$('.select2-container').off('click.container')
.on('click.container', function(event) {
event.stopPropagation();
})
});
},
scroll: (target, options) => {
if (typeof target === 'string') {
$(target).mCustomScrollbar(options)
} else {
$(target[0]).mCustomScrollbar('scrollTo', target[1], {
scrollOffset: [$(target[0]), 3, 4]
})
}
},
arialabel: () => {
let arialabel = 'aria-label';
document.querySelectorAll('[data-tooltip="mailbox"]:not(' + arialabel + ')').forEach(
t => t.setAttribute(arialabel, t.getAttribute('data-title'))
)
},
tooltip: () => {
$('[data-tooltip="mailbox"]').tooltip({
html: true,
trigger: 'hover',
container: 'body',
sanitize: false,
delay: {
show: 600,
hide: 30
}
});
}
},
},
/* jshint +W117 */
/**
* Defines component template
*
* @since 19.20
*
* @return {string|object}
*/
$$ = {
$: {
/**
* Returns set of selectors for generating layout
*
* @returns {object}
*/
layout: {
container: 'container-fluid',
controls: 'mail-controls',
panel: 'panel-mail panel-body',
row: {
controls: 'row row-controls',
messages: 'row row-messages colorify',
quota: 'row row-quota',
centered: 'row text-center',
},
column: {
3: 'col-xs-3',
4: 'col-xs-4',
6: 'col-xs-6',
8: 'col-xs-8',
9: 'col-xs-9',
12: 'col-xs-12',
},
button: {
link: 'btn btn-link text-decoration-none',
transparent: {
plain: 'btn btn-transparent',
link: 'btn btn-link btn-transparent',
},
default: 'btn btn-default',
primary: 'btn btn-primary',
block: {
default: 'btn btn-default btn-block',
transparent: 'btn btn-transparent btn-block'
},
dropdown: {
default: 'btn btn-default dropdown-toggle'
}
}
},
/**
* Returns used selectors for generating elements
*
* @returns {string|object}
*/
tree: {
container: 'data-mail-folders',
active: 'fancytree-active',
loader: 'fancytree-loader',
title: 'fancytree-title',
bubble: 'label label-danger',
},
controls: {
compose: {
button: '[data-compose]',
icon: 'fa-fw fa-plus',
},
select: {
dropdown: 'dropdown-select',
checkbox: '[data-select] input',
menus: '[data-select-mass]',
},
delete: 'btn btn-default fa fa-trash',
forward: 'btn btn-default fa fa-forward',
search: {
link: '[data-href^="sort.cgi"]',
clear: {
link: 'search-clear text-danger',
icon: 'fa-fw fa-times-circle-o',
},
dropdown: 'dropdown-search',
icon: 'fa-search',
data: {
form: {
action: 'data-form-action',
type: 'data-form-action-type',
advanced: 'data-form-action-advanced'
},
},
button: {
type: '[data-toggle-type="1"]',
},
caret: {
down: 'fa-caret-down',
up: 'fa-caret-up',
},
submit: '[data-search-submit]',
},
move: {
dropdown: 'dropdown-move',
checkbox: '[data-copy-only]',
icon: 'fa-folder-move',
submit: '[data-transfer-submit]',
},
more: {
dropdown: 'dropdown-more',
icon: 'fa-dots-vertical',
menu: {
read: '[data-form-action="markas1"]',
unread: '[data-form-action="markas0"]',
special: '[data-form-action="markas2"]',
spam: '[data-form-action="razor"]',
ham: '[data-form-action="ham"]',
black: '[data-form-action="black"]',
white: '[data-form-action="white"]',
},
},
sort: {
dropdown: 'dropdown-sort',
icon: 'fa-fw fa-sort',
},
counter: 'mail-selected-count',
refresh: {
button: 'btn btn-transparent-link btn-lg btn-transparent fa fa-refresh',
animation: 'fa-spin-gradual'
},
pagination: 'pagination-title',
settings: 'btn btn-default fa fa-cog'
},
messages: {
checkbox: 'input[data-check]',
flag: 'mail-list-trow-flag-security',
special: {
star: 'star',
starred: 'fa-star star',
unstarred: 'fa-star-o star',
},
row: {
empty: 'fa fa-fw fa-1_50x fa-inbox'
}
},
},
/**
* Creates HTML element specified by type
*
* @since 19.20
*
* @param {string} class Class name for created element
* @param {mixed} data Data attributes to pass
* @param {string} [type] Used tag name
* @param {string} [content] Content of created element
* @param {string} [icon] Icon class name to use
* @param {string} [tooltip] Element tooltip title to show on hover
*/
create: {
/**
* Generates element with chosen tag name
*
* @returns {string}
*/
$: function(classes, data, type = 'div', content = String(), tooltip = String()) {
let attributes = this._attributes(data);
classes = this._classes(classes);
if (tooltip) {
tooltip = 'data-tooltip="mailbox" data-placement="bottom" data-title="' + tooltip + '"'
}
return '<' + type + ' ' + attributes + ' ' + tooltip + ' class="' + classes + '">' + content + '</' + type + '>';
},
/**
* Generates icon element
*
* @returns {string}
*/
icon: function(classes, attributes = String()) {
let attribute = this._attributes(attributes),
icon = this._classes(classes);
return '<i class="fa ' + icon + ' ' + attribute + '"></i>';
},
/**
* Generates button element
*
* @returns {string}
*/
button: function(classes, data, content, icon, tooltip) {
icon = this._classes(icon);
return this.$(classes, data, 'button', ((icon ? '<i class="fa ' + icon + '"></i> ' : '') + content + ''), tooltip);
},
/**
* Generates input element
*
* @returns {string}
*/
input: function(name = String(), placeholder = String(), value = String(), type = 'text', attributes = String()) {
let attribute = this._attributes(attributes);
return '<input ' + attribute + ' type="' + type + '" name="' + name + '" id="' + name + '" placeholder="' + placeholder + '" value="' + value + '">';
},
/**
* Generates label element
*
* @returns {string}
*/
label: function(target = String(), content = String(), attributes = String()) {
let attribute = this._attributes(attributes);
return '<label ' + attribute + ' for="' + target + '">' + content + '</label>';
},
/**
* Generates select element
*
* @returns {string}
*/
select: function(data, attributes = String(), name = String()) {
let attribute = this._attributes(attributes),
select = '<select ' + attribute + ' name="' + name + '">';
for (let [value, text] of Object.entries(data[0])) {
select += '<option value="' + value + '"' + (data[1] && data[1] == value ? ' selected' : String()) + '>' + text + '</option>';
}
select += '</select>';
return select;
},
/**
* Generates checkbox element
*
* @returns {string}
*/
checkbox: function(attributes = String(), name = String(), value = String(), label = ' ', id = String(), checked = String()) {
let attribute = this._attributes(attributes),
checkbox = String();
checkbox += '<span ' + attribute + ' class="awcheckbox awobject">';
checkbox += '<input class="iawobject" ' + checked + ' type="checkbox" name="' + name + '" value="' + value + '" id="' + id + '">';
checkbox += '<label class="lawobject" for="' + id + '">' + label + '</label>';
checkbox += '</span>';
return checkbox;
},
/**
* Generates radio element
*
* @returns {string}
*/
radio: function(attributes = String(), name = String(), value = String(), label = ' ', id = String(), checked = String()) {
let attribute = this._attributes(attributes),
checkbox = String();
checkbox += '<span ' + attribute + ' class="awradio awobject">';
checkbox += '<input class="iawobject" ' + checked + ' type="radio" name="' + name + '" value="' + value + '" id="' + id + '">';
checkbox += '<label class="lawobject" for="' + id + '">' + label + '</label>';
checkbox += '</span>';
return checkbox;
},
/**
* Generates dropdown element
*
* @returns {string}
*/
dropdown: function(classes, data, button, icon, tooltip) {
let dropdown = String();
classes = this._classes(classes);
dropdown += '<div class="btn-group ' + classes + '">';
if (button) {
if (/<[a-z][\s\S]*>/i.test(button)) {
dropdown += this.$('layout.button.default', false, 'span', button);
} else {
dropdown += this.button('layout.button.default', false, button);
}
}
dropdown += this.button('layout.button.dropdown.default', {
'toggle': 'dropdown'
}, '<span class="' + (icon ? ('fa ' + $$.$.controls[icon].icon) : 'caret') + '"></span>', false, tooltip);
dropdown += '<ul class="dropdown-menu" role="menu">';
for (let [i, v] of data[0].entries()) {
if (v) {
dropdown += '<li>' + v + '</li>';
}
if (data[i - 1] && data[1] && data[1] === (i + 1) && data[0].length > data[1]) {
dropdown += '<li role="separator" class="divider"></li>';
}
}
dropdown += '</ul>';
dropdown += '</div>';
return (data[0].length ? dropdown : String());
},
/**
* Converts passed object of attributes to string representation
*
* @param {object} data Array or hash to convert to string
*
* @example ['href="value"', 'title="value"'] or {type: value, title: value}.
*
* @returns {string}
*/
_attributes: function(data) {
let attributes = String();
if (Array.isArray(data)) {
attributes = data.join(' ');
} else if (data) {
attributes = Object.entries(data).map(([k, v]) => (attributes += (k.startsWith('data-') ? k : ('data-' + k)) + '=' + v + ' ')).slice(-1)[0];
}
return attributes;
},
/**
* Converts passed selector to correspondent class name from the component template
*
* @returns {string}
*/
_classes: function(classes) {
return classes ? (classes.split('.').reduce((a, b) => {
return a ? a[b] : undefined;
}, $$.$)) : String();
},
},
/**
* Returns selector name derived from the component template
*
* @returns {string}
*/
selector: function(selector) {
selector = this.create._classes(selector);
return selector.startsWith('[') ? selector :
"." + selector.replace(/\s+/g, ".")
},
/**
* Returns DOM object based on passed selector
*
* @returns {object}
*/
element: function(element) {
return element ? $(this.selector(element)) : String();
},
}
/**
* Messages object sub-module ;;
*
* @since 19.20
*
* @return {object} Reveals messages module API
* @return {void} messages.get Lists messages with default sorting
* @return {void} messages.sort Lists messages with requested sorting
* @return {object} messages.storage Accesses messages data storage
*/
const messages = (function() {
/**
* Fetches and renders list of messages for the given folder with particular pagination
*
* @returns {void}
*/
const get = (data) => {
loader.start();
_.notification('hide-all');
$.post(_.path.extensions + '/mail/messages.cgi?' + _.plugin.json_to_query(data),
function(data) {
render(data);
loader.end();
_.document_title(0, _.language('theme_xhred_titles_mail'));
});
},
/**
* Displays loader while loading messages
*
* @returns {void}
*/
loader = {
target: $$.selector('tree.active'),
start: function() {
$(this.target).addClass($$.$.tree.loader)
},
end: function() {
$(this.target).removeClass($$.$.tree.loader)
}
},
/**
* Defines the length of the preview to request from the server
*
* @returns {number}
*/
preview_length = () => {
return parseInt($(window).width() / 10);
},
/**
* Holds temporary data storage for managing selected messages across different pages
*
* @returns {object}
*/
storage = {
target: '[' + $$.$.tree.container + ']',
counter: $$.selector('controls.counter'),
/**
* Gets currently selected messages and its data
*
* @returns {object|array}
*/
get: function(status = 0) {
let data = $(this.target).data('messages') || {};
if (!status) {
data = Object.keys(data);
}
return data;
},
/**
* Stores just checked/unchecked message and its data
*
* Updates selected messages counter, and controls display
*
* @returns {void}
*/
set: function(id, state, status, starred, data) {
let storage = this.get(1);
// Process messages
state ? storage[id] = [+status, +starred] : delete storage[id]
// Set current messages storage
$(this.target).data('messages', storage);
// Update counter
let selected_count = Object.keys(storage).length;
$(this.counter).text(
(
selected_count ? (selected_count + ' ' + _.language('theme_xhred_global_selected')) : String()
)
.toLowerCase()
);
// Show/hide control row
let controls = $$.selector('layout.controls');
$(controls).toggleClass('hidden', !selected_count);
},
/**
* Restores messages selection upon listing
*
* @returns {void}
*/
restore: function() {
let data = this.get(),
checkboxes = $$.$.messages.checkbox;
$(checkboxes).filter((i, t) => {
data.includes(t.value) && $(t).prop('checked', 1)
}).promise().done(function() {
$(checkboxes).trigger('change');
});
},
/**
* Resets messages selection storage
*
* @returns {void}
*/
reset: function() {
let checkboxes = $$.$.messages.checkbox;
$(this.target).data('messages', {})
$(checkboxes + ':checked').prop('checked', 0).trigger('change');
},
},
/**
* Updates message(s) read/unread status
*
* @returns {object}
*/
status = {
/**
* Extracts action name based on type
*
* @returns {string}
*/
action: function(action, string = false) {
action = parseInt((action).replace(/^\D+/g, ''));
if (string) {
return action ? 'read' : 'unread';
}
return action
},
/**
* Sets message read/unread state in UI
*
* @returns {void}
*/
set: function(action, messages) {
let $messages = $($$.$.messages.checkbox).filter((i, c) => {
return messages.includes(c.value)
}),
$targets = $messages.parent().parents('td').parents('tr');
$targets.attr('data-unread', +!this.action(action))
},
/**
* Writes message read/unread state to the server
*
* @returns {void}
*/
write: function(data, messages = false) {
let folder_index = data[1].searched_folder_index || data[1].folder_index;
if (messages) {
let action = this.action(data[0], 1);
messages = `&d=${messages.join('&d=')}`;
$.post(_.path.extensions + '/mail/message.cgi?folder=' + folder_index + '&mark=' + action + messages + '');
} else {
let action = this.action(data[0], 1),
server = data[1],
messages = storage.get(1),
starred = {
read: [],
unread: []
};
// Filter out starred messages
$.each(messages, function(i, o) {
if (o[1] === 1) {
o[0] === 1 ? starred.unread.push(i) : starred.read.push(i)
delete messages[i];
}
});
// Submit ordinary data
submit(server, {
[data[0]]: 1
}, Object.keys(messages), 0, 1);
// Submit data for incompatible states
let link = _.path.extensions + '/mail/message.cgi?folder=' + folder_index + '&mark=starred&state=' + action + '';
starred.read.length &&
$.post(link + `&d=${starred.read.join('&d=')}` + '');
starred.unread.length &&
$.post(link + '' + `&d=${starred.unread.join('&d=')}` + '');
}
},
},
/**
* Register events
*
* @returns {void}
*/
events = (data) => {
// Import targets
let button = {
compose: $$.$.controls.compose.button,
search: $$.element('controls.search.dropdown'),
refresh: $$.element('controls.refresh.button'),
delete: $$.element('controls.delete'),
forward: $$.element('controls.forward'),
special: {
star: $$.selector('messages.special.star'),
starred: $$.selector('messages.special.starred'),
unstarred: $$.selector('messages.special.unstarred'),
}
},
dropdown = {
mark: {
read: $$.element('controls.more.menu.read'),
unread: $$.element('controls.more.menu.unread'),
special: $$.element('controls.more.menu.special'),
spam: $$.element('controls.more.menu.spam'),
ham: $$.element('controls.more.menu.ham'),
black: $$.element('controls.more.menu.black'),
white: $$.element('controls.more.menu.white'),
},
select: $$.selector('controls.select.dropdown'),
move: $$.element('controls.move.dropdown'),
search: $$.element('controls.search.dropdown')
},
checkbox = $($$.$.controls.select.checkbox),
checkboxes = $$.$.messages.checkbox,
flags = $$.selector('messages.flag');
/**
* Event listeners for selecting all messages
*
* @returns {void}
*/
checkbox.on('change', function() {
let $this = $(this),
state = $this.is(':checked');
$(checkboxes).prop('checked', state).trigger('change');
}).parent().parent().on('click', function(event) {
let $input = $(this).find('input');
!$(event.target).is($input) && $input.prop('checked', !$input.is(':checked')).trigger('change');
})
/**
* Event listeners for selecting all messages
*
* @returns {void}
*/
$(flags).on('click', function(event) {
event.stopPropagation()
})
/**
* Event listener for selecting specific type of multiple messages
*
* @returns {void}
*/
$(dropdown.select).find($$.$.controls.select.menus).on('click', function(event) {
let _$ = $(event.target).data('type'),
$_ = $(checkboxes),
$__ = 'change',
__$ = 'checked';
// Select all/none
if (_$ === 5 || _$ === 4) {
$_.prop(__$, (_$ & 1)).trigger($__);
}
// Select invert
else if (_$ === 3) {
$_.prop(__$, function() {
return !this.checked
}).trigger($__);
}
// Select read/unread
else if (_$ === 2 || _$ === 1) {
$_.prop(__$, function() {
return +$(this).parents('tr').attr('data-unread') === (_$ & 1)
}).trigger($__);
}
// Select starred (special)
else if (_$ === 0) {
$_.prop(__$, function() {
return +$(this).parents('tr').attr('data-starred') === +!(_$ & 1)
}).trigger($__);
}
})
/**
* Event listener for selecting single message
*
* Updates the storage data
*
* @returns {void}
*/
$(checkboxes).on('change', function() {
let $this = $(this),
$row = $this.parents('td').parent('tr'),
state = $this.is(':checked'),
id = $this.val(),
checked = (checkboxes + ':checked'),
status = $row.attr('data-unread'),
starred = $row.attr('data-starred');
storage.set(id, state, status, starred, data);
$(checked).length === $(checkboxes).length ? checkbox.prop('checked', 1) : checkbox.prop('checked', 0);
});
/**
* Event listener for deleting message(s)
*
* @returns {void}
*/
button.delete.on('click', function() {
submit(data, {
'delete': 1
}, storage.get(), 1, 1)
});
/**
* Event listener for moving/copying message(s)
*
* @returns {void}
*/
let $dropdown_move_select = dropdown.move.find('select');
$dropdown_move_select.find('option').map((i, o) => {
o.value <= -1 && o.remove()
})
_.plugin.select($dropdown_move_select);
$dropdown_move_select.on('change', function() {
setTimeout(() => {
$($$.$.controls.move.submit).toggleClass('disabled', !this.value).trigger('focus');
});
})
dropdown.move.find('li').on('click', function(event) {
event.stopPropagation();
let $target = $(event.target),
$submit = $($$.$.controls.move.submit),
$copy = $($$.$.controls.move.checkbox),
copy = $copy.is(':checked');
if ($target.is($copy)) {
$submit.text(copy ? _.language('theme_xhred_global_copy') : _.language('theme_xhred_global_move'));
}
if ($target.is('button:not(.disabled)')) {
let action = copy,
target = parseInt($dropdown_move_select.val());
submit(data, {
[(action ? 'copy' : 'move') + '1']: 1,
mfolder1: target
}, storage.get(), (+!action || ((data.searched_folder_index || data.folder_index) === target)), 1)
dropdown.move.removeClass('open')
}
})
dropdown.move.on('shown.bs.dropdown', function() {
_.plugin.select([$dropdown_move_select, 'open']);
})
/**
* Event listener for forwarding message(s)
*
* @returns {void}
*/
button.forward.on('click', function() {
// Produce notification (temporary)
_.notification(['exclamation-triangle', 'Forward functionality is no yet implemented. Expect it in the future beta pre-release.'], 10, "info", 0, 1, ['top', 'center'])
});
/**
* Event listener for search
*
* @returns {void}
*/
let $dropdown_search_select = dropdown.search.find('select'),
$dropdown_search_simple = dropdown.search.find('[data-search-mail]'),
$dropdown_search_advanced_all = dropdown.search.find('[name="search-wordsin"]');
// Set current folder first
if ($dropdown_search_select.length) {
$dropdown_search_select[0].value = data.searched_folder_index || data.folder_index;
}
// Initialize folders select
_.plugin.select($dropdown_search_select);
button.search.find('li').on('click keyup', function(event) {
event.stopPropagation();
let $target = $(event.target),
$advanced_form = dropdown.search.find('[' + $$.$.controls.search.data.form.advanced + ']'),
advanced_form_hidden = () => window.getComputedStyle($advanced_form[0]).display === 'none';
// Close and return
if (event.keyCode === 27) {
button.search.trigger('click')
return
}
// Show/hide advanced search options
if (
(event.keyCode === 32 ||
event.keyCode === 9 ||
event.keyCode === 13 ||
event.type === 'click') &&
$target.is($($$.selector('controls.search.caret.down')).add($$.selector('controls.search.button.type')))
) {
let $caret = $target.is('i') ? $target : $target.find('i');
$caret.toggleClass($$.$.controls.search.caret.up)
$advanced_form.toggleClass('show');
$dropdown_search_simple[0].disabled = !advanced_form_hidden();
let focus_target = advanced_form_hidden() ? $dropdown_search_simple : $advanced_form[0].querySelector('input');
focus_target.focus();
if (advanced_form_hidden()) {
$dropdown_search_simple.val($dropdown_search_advanced_all.val())
$dropdown_search_advanced_all.val(String())
} else {
$dropdown_search_advanced_all.val($dropdown_search_simple.val())
$dropdown_search_simple.val(String())
}
return
}
// Submit search query
if (
(event.type === 'keyup' && event.keyCode === 13 && $target.is('[type="text"], [type="number"]')) ||
(event.type === 'click' && $target.is('button:not(.disabled)'))
) {
let simple_query = {
folder: parseInt($dropdown_search_select[0].value),
search: $dropdown_search_simple[0].value
};
// Submit simple search query
if (advanced_form_hidden()) {
if (simple_query.search) {
$.post(_.path.prefix + '/' + _.variable.module.name() + '/mail_search.cgi?returned_format=json&json-error=1&simple=1&' + _.plugin.json_to_query(simple_query), function(data) {
messages.get(data);
});
}
}
// Submit advanced search query
else {
let $elements_input = $advanced_form.find('input[type="text"]').filter((i, v) => v.value),
$elements_radios_status = $advanced_form.find('input[name="status"]'),
$elements_limit = $advanced_form.find('input[name^="limit"]'),
$elements_attach = $advanced_form.find('input[name="attach"]'),
query = {};
// Default query params
query.all = 1;
query.dest_def = 1;
query.folder = simple_query.folder;
// Create query for all input fields
for (let i = 0; i < $elements_input.length; i++) {
let value = $elements_input[i].value,
special = $elements_input[i].name.includes('words'),
name = $elements_input[i].name.replace('search-', String());
if (value) {
query['what_' + i] = value;
if (special) {
query['field_' + i] = 'all';
query['neg_' + i] = ~~$elements_input[i].name.includes('out');
} else {
query['field_' + i] = name;
query['neg_' + i] = 0;
}
}
}
// Create query with status radios
query.status_def = 0;
for (let i = 0; i < $elements_radios_status.length; i++) {
let $this = $elements_radios_status[i],
value = $this.value,
def = value == -1 ? 1 : 0;
if ($this.checked) {
query.status_def = def;
if (!def) {
query.status = value;
}
}
}
// Create query with limit radios
for (let i = 0; i < $elements_limit.length; i++) {
let $this = $elements_limit[i],
name = $this.name;
if ($this.checked && $this.name === 'limit_def' || $this.name !== 'limit_def') {
query[name] = $this.value
}
}
// Create query with attachment status
query.attach = ~~$elements_attach[0].checked;
// Run the query
$.post(_.path.prefix + '/' + _.variable.module.name() + '/mail_search.cgi?returned_format=json&json-error=1&' + _.plugin.json_to_query(query), function(data) {
messages.get(data);
});
}
}
});
dropdown.search.on('shown.bs.dropdown', function() {
$dropdown_search_simple.trigger('focus');
})
/**
* Event listener for refreshing messages list
*
* @returns {void}
*/
button.refresh.on('click', function() {
$(this).addClass($$.$.controls.refresh.animation);
$$.element('tree.active').click()
})
/**
* Event listener for marking message starred/unstarred (toggle special state)
*
* @returns {void}
*/
$(button.special.star).on('click', function(event) {
event.stopImmediatePropagation();
let $this = $(this),
$row = $(event.target).parents('td').parent('tr'),
target = $$.$.messages.special,
id = $row.find('input[value]').val(),
state = $(event.target).is($(button.special.starred)) ? 1 : 0,
unread = +$row.attr('data-unread'),
text = _.language('theme_xhred_global_' + (state ? 'unstarred' : 'starred') + '');
$row.attr('data-starred', +!state);
// Submit changes and toggle state
submit(data, {
['markas' + (state ? 1 : 2) + '']: 1
}, [id])
$this
.removeClass(target[(state ? 'starred' : 'unstarred')])
.addClass(target[(state ? 'unstarred' : 'starred')])
.attr('data-original-title', text)
.next().remove();
// Write message status (redundant)
status.write([(+!unread).toString(), data], [id]);
});
/**
* Event listener for marking message(s) read/unread
*
* @returns {void}
*/
dropdown.mark.read
.add(dropdown.mark.unread)
.on('click', function() {
let action = $(this).data('form-action'),
messages = storage.get();
// Write message status (redundant)
status.write([action, data]);
// Change messages UI state
status.set(action, messages);
});
/**
* Event listener for reporting spam/ham and whitelisting/blacklisting message(s)
*
* @returns {void}
*/
dropdown.mark.spam
.add(dropdown.mark.ham)
.add(dropdown.mark.black)
.add(dropdown.mark.white)
.on('click', function() {
let action = $(this).data('form-action'),
messages = storage.get(),
refetch = /razor|black/.test(action);
submit(data, {
[action]: 1
}, messages, +refetch, 1);
})
/**
* Event listener for running search
*
* @returns {void}
*/
$($$.$.controls.search.link).on('click', function() {
let link = this.getAttribute('data-href');
fetch(link)
.then(function(response) {
return response.json();
})
.then(function(data) {
messages.get(data)
});
})
/**
* Event listener for composing new message
*
* @returns {void}
*/
$(folders.data.selector.navigation)
.off('click', button.compose)
.on('click', button.compose, function() {
_.content(_.variable.module.link() + '/reply_mail.cgi?new=1&folder=0');
})
},
/**
* Submits changes to the server
*
* @param {object} data Response object with data for current page
* @param {object} actions Action(s) to be submitted
* @param {object} messages Array of message ids to process
* @param {int} [refetch] Refetch current folder's content from the server
* @param {int} [reset] Reset message selection
*
* @returns {void}
*/
submit = (data, actions, messages, refetch = 0, reset = 0) => {
let form = data.form_list,
target = _.variable.module.link() + `/${form.target}?`,
hidden = form.hidden,
searched_index = data.searched_folder_index,
mail_system = parseInt(data.mail_system);
hidden = _.plugin.json_to_query(hidden) + '&noredirect=1&';
// Focus actual folder instead of virtual
if (searched_index && (mail_system === 2 || mail_system === 4)) {
hidden = hidden.replace(/folder=\d+/, `folder=${searched_index}`)
}
actions = _.plugin.json_to_query(actions);
messages = `&d=${messages.join('&d=')}`;
refetch && (loader.start(), _.notification('hide-all'));
$.post(target + hidden + actions + encodeURI(messages), function() {
if (reset) {
storage.reset();
}
if (refetch || data.folder_counts_allowed) {
$.post(_.path.extensions + '/mail/messages.cgi?' + hidden + 'show_body_len=' + preview_length() + '', function(data) {
render(data);
loader.end();
});
}
});
},
/**
* Render static page
*
* @param {string} page Returns HTML for static page
*
* @returns {string}
*/
row = (text, icon) => {
let row = String(),
centered_row = $$.create.$('layout.row.centered');
row = $(centered_row)
.append((icon ? $$.create.icon(icon) : String()) +
'<div class="text-uppercase"> ' + text + ' </div>');
return row;
},
/**
* Render messages and controls
*
* @param {object} source Response object with data for current page
*
* @returns {void}
*/
render = (source) => {
let container = $$.element('layout.container'),
data = source[0],
messages_list = ((data.list.messages && data.form_list.buttons) ? data.list.messages.replace(/�/g, '') : String());
// Check for errors first
if (data.error) {
let errors = data.error.error;
for (let i = 0; i < errors.length; i++) {
_.notification(['exclamation-circle', errors[i]], 20, "error", i, 1, ['top', 'center']);
}
return
}
let messages_list_available = messages_list.length > 128 ? 1 : 0;
if (!messages_list_available && data.searched) {
_.notification(['search', _.language('theme_xhred_mail_search_empty')], 5, "info", 0, 1, ['top', 'center'])
return
}
// Empty current panel and define target
container.empty().append($$.create.$('layout.panel'));
let panel = container.find($$.selector('layout.panel'));
// Inject data to the panel
if (messages_list_available) {
let controls = {
select: data.form_list.buttons.select,
submit: data.form_list.buttons.submit
},
pagination = {
link: (data.pagination_arrow_last || data.pagination_arrow_first || String()),
title: (data.pagination_arrow_last ? _.language('theme_xhred_mail_pagination_last') : (data.pagination_arrow_first ? _.language('theme_xhred_mail_pagination_first') : false))
}
panel
.append($$.create.$('layout.row.controls'))
.find($$.selector('layout.row.controls'))
.append($$.create.$('layout.column.6'), $$.create.$('layout.column.6'))
.find($$.selector('layout.column.6')).first()
.append($$.create.dropdown('controls.select.dropdown', [
[
controls.select.all,
controls.select.none,
controls.select.invert,
controls.select.read,
controls.select.unread,
controls.select.special
], 3
], $$.create.checkbox({
select: 1
}), String(), _.language('theme_xhred_global_select')));
let $form_controls = $($$.create.$('layout.controls', {
'form-controls': 1
}, 'div'));
Object.entries(controls.submit).map(([type, data]) => {
for (let [i, v] of data.entries()) {
if (type === 'buttons') {
$form_controls.append($$.create.$('controls.' + v[0], {
'form-control': v[0]
}, 'span', String(), _.language('theme_xhred_global_' + v[0] + '')));
} else if (type === 'dropdowns') {
for (let [di, dd] of v.entries()) {
let entries = [];
for (let [index, data] of v[1].entries()) {
data[0] && entries.push($$.create.$(0, {
'form-action': data[0]
}, 'span', data[1]));
}
if (typeof dd === "string") {
$form_controls.append(
$$.create.dropdown('controls.' + dd + '.dropdown', [
entries, 2
], 0, dd, _.language('theme_xhred_mail_' + dd + '') || _.language('theme_xhred_global_' + dd + ''))
)
}
}
}
}
});
panel
.find($$.selector('layout.column.6')).first()
.append(
$form_controls,
$$.create.dropdown('controls.sort.dropdown', [
[
data.list.sort.date,
data.list.sort.from,
data.list.sort.size,
data.list.sort.subject,
data.list.sort.spam,
], 5
], data.list.sorted, 'sort', _.language('theme_xhred_global_sort')),
$$.create.dropdown('controls.search.dropdown', [
[
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.type]: 'simple'
}, 'span',
(
$$.create.input('search', _.language('theme_xhred_mail_search_search_mail'), String(), 'text', {
'search-mail': 1
}) +
$$.create.button('layout.button.transparent.link', {
'toggle-type': 1
}, String(), 'controls.search.caret.down')
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1
}, 'span',
(
$$.create.$('layout.column.3', {}, 'span', $$.create.label('search-from', _.language('theme_xhred_mail_search_from'))) +
$$.create.$('layout.column.9', {}, 'span', $$.create.input('search-from'))
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-to', _.language('theme_xhred_mail_search_to'))) +
$$.create.$('layout.column.9', 0, 'span', $$.create.input('search-to'))
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-subject', _.language('theme_xhred_mail_search_subject'))) +
$$.create.$('layout.column.9', 0, 'span', $$.create.input('search-subject'))
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-wordsin', _.language('theme_xhred_mail_search_has_words'))) +
$$.create.$('layout.column.9', 0, 'span', $$.create.input('search-wordsin'))
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-wordsout', _.language('theme_xhred_mail_search_doesnt_have_words'))) +
$$.create.$('layout.column.9', 0, 'span', $$.create.input('search-wordsout'))
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-status', _.language('theme_xhred_mail_search_with_status'))) +
$$.create.$('layout.column.9', 0, 'span',
$$.create.radio(0, 'status', -1, _.language('theme_xhred_mail_search_with_status_any'), 'status_def', 'checked') +
$$.create.radio(0, 'status', 0, _.language('theme_xhred_mail_search_with_status_unread'), 'status0') +
$$.create.radio(0, 'status', 1, _.language('theme_xhred_mail_search_with_status_read'), 'status1') +
$$.create.radio(0, 'status', 2, _.language('theme_xhred_mail_search_with_status_special'), 'status2')
)
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1,
[$$.$.controls.search.data.form.type]: 'search-in'
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-folder', _.language('theme_xhred_mail_search_search_in'))) +
$$.create.$('layout.column.9', 0, 'span', data.form_list.buttons.submit.dropdowns[0][1][0][1])
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1,
[$$.$.controls.search.data.form.type]: 'limit'
}, 'span',
(
$$.create.$('layout.column.3', 0, 'span', $$.create.label('search-wordsout', _.language('theme_xhred_mail_search_limit_results'))) +
$$.create.$('layout.column.9', 0, 'span',
$$.create.radio(0, 'limit_def', 1, _.language('theme_xhred_global_no'), 'limit_def0', 'checked') +
$$.create.radio(0, 'limit_def', 0, _.language('theme_xhred_mail_search_limit_results_yes') + ' ' + $$.create.input(
'limit', '', 20, 'number', ['step="20"', 'min="10"']) + ' latest messages', 'limit_def1')
)
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1,
[$$.$.controls.search.data.form.type]: 'attach'
}, 'span',
(
$$.create.checkbox(0, 'attach', 1, _.language('theme_xhred_mail_search_has_attach'), 'attach')
)
),
$$.create.$(0, {
[$$.$.controls.search.data.form.action]: 'search',
[$$.$.controls.search.data.form.advanced]: 1,
[$$.$.controls.search.data.form.type]: 'submit'
}, 'span',
(
$$.create.button('layout.button.primary', false, _.language('theme_xhred_global_search'), 'controls.search.icon')
)
)
], 5
], (data.searched_message ?
$$.create.$(
'controls.search.clear.link', ['href="index.cgi?folder=' + data.searched_folder_index + '"'],
'a',
($$.create.icon('controls.search.clear.icon') + ' ' + data.searched_message.toLowerCase()),
_.language('theme_xhred_mail_search_clear')) : String()
), 'search', _.language('theme_xhred_global_search')),
$$.create.$('controls.counter', 0, 'span')
)
.end().last()
.append(
$$.create.$('controls.refresh.button', {
'refresh': 1
}, 'span', String(), _.language('theme_xhred_global_refresh')),
$$.create.$('controls.pagination', (pagination.link ? ['href="' + pagination.link + '"', 'data-href="' + pagination.link + '"'] : false), 'a', data.pagination_message, pagination.title),
data.pagination_arrow_left,
data.pagination_arrow_right
)
panel
.append($$.create.$('layout.row.messages')).find($$.selector('layout.row.messages'))
.append($$.create.$('layout.column.12')).find($$.selector('layout.column.12'))
.append(messages_list)
if (data.quota) {
panel
.append($$.create.$('layout.row.quota')).find($$.selector('layout.row.quota')).last()
.append($$.create.$('layout.column.12')).find($$.selector('layout.column.12'))
.append(data.quota)
}
_.plugin.timestamp();
_.plugin.arialabel();
_.plugin.tooltip();
_.plugin.offset_adjust(true);
_.rows();
folders.set(data);
folders.update(data);
events(data);
messages.storage.restore();
} else {
events();
panel.append(row((data.folder_index === 0 ? _.language('theme_xhred_mail_no_new_mail') : _.language('theme_xhred_mail_no_mail')), 'messages.row.empty'))
}
}
// Reveal sub-modules ;;
return {
get: get,
storage: storage,
}
})()
/**
* Folders sub-module ;;
*
* @since 19.17
*
* @return {object} Reveals folders module API
* @return {void} folders.get Retrieve mail folders
* @return {void} folders.set Mark folder as active
* @return {void} folders.adjust Adjust active folder into view
* @return {object} folders.data Returns module properties
*/
const folders = (function() {
let
// Define module static properties
data = {
file: {
fancytree: 'jquery.fancytree'
},
selector: {
navigation: 'aside .navigation'
},
options: {
tree: {
escapeTitles: false,
autoActivate: false,
autoScroll: true,
keyboard: false,
toggleEffect: false,
},
scroll: {
axis: 'xy',
theme: 'minimal',
keyboard: false,
scrollInertia: 300,
scrollButtons: true,
autoHideScrollbar: false,
}
},
plugin: {
tree: (source) => {
source = (source === 'get' ? 'getTree' :
(source === 'node' ? 'getActiveNode' :
Object.assign(data.options.tree, {
source: source,
scrollParent: $('[' + $$.$.tree.container + ']'),
click: (e, d) => {
if (d.targetType === 'title') {
setTimeout(() => {
tree.adjust();
}, 1e2);
_.content(data.url.link + encodeURIComponent(d.node.key));
messages.storage.reset();
_.navigation.reset();
}
}
})));
if ($(tree.container).length) {
return $(tree.container).fancytree(source);
}
}
},
url: {
link: _.path.origin + _.path.prefix + '/mailbox/index.cgi?id=',
}
};
/**
* Tree sub-module ;;
*
* @return {string|function}
*/
let tree = {
fetched: 0,
container: '[' + $$.$.tree.container + ']',
container_adjust: function() {
let container = $(this.container + ' >:first'),
content = $(this.container + ' >>:first');
if (container.height() > content.height()) {
container.css('height', content.height())
}
},
init: function(source) {
// Load dependencies
if (this.fetched === 0) {
this.load();
return;
}
// Insert tree container and compose button
if ($(data.selector.navigation + ' ' + this.container).length === 0) {
$(data.selector.navigation).prepend('<li><div ' + $$.$.tree.container + '></div></li>');
$(data.selector.navigation).prepend('<li>' + $$.create.$('layout.button.block.transparent', {
'compose': 1
}, 'span', $$.create.icon('controls.compose.icon') + " " + _.language('theme_xhred_mail_new_message')) + '</li>');
} else {
return;
}
// Instantiate tree
data.plugin.tree(source);
// Make the container scrollable
_.plugin.scroll(this.container, data.options.scroll);
// Adjust container height
this.container_adjust();
},
expand: function(node) {
let expanded = node.isExpanded();
!expanded && node.toggleExpanded();
},
load: function() {
this.fetched = 1;
_.load.bundle(_.path.js + '/' + data.file.fancytree,
_.path.css + '/' + data.file.fancytree,
(_.variable.switch() ? [get] : 0), 1
);
},
reload: function(source) {
let tree = data.plugin.tree('get');
tree.$container.empty();
tree.reload(source);
setTimeout(() => {
this.adjust();
}, 1e2);
},
node: function() {
return data.plugin.tree('node');
},
adjust: function() {
let $_ = this.node();
if ($_ && $_.li && $($_.li).length) {
_.plugin.scroll([this.container, $($_.li)]);
}
this.container_adjust();
}
}
/**
* Retrieves mail folders
*
* @param {string} [key] Folder name to be set as active
*
* @return {void}
*/
const get = (key) => {
key = key ? ('?key=' + key.replace(/&/g, '%26')) : String();
$.post(_.path.extensions + '/mail/folders.cgi' + key + '', function(source) {
if (!!key) {
tree.reload(source)
} else {
tree.init(source)
}
});
}
/**
* Mark mail folder as active
*
* @param {string|object} key Extract folder name to set as active
*
* @return {void}
*/
const set = function(key) {
let tree = data.plugin.tree('get');
// Detect source
if (typeof key === 'object') {
let search = {
id: key.searched_folder_id,
file: key.searched_folder_file,
},
id = key.folder_id;
// Sey active folder
if (search.file && search.id != null && key.mail_system != 2 && key.mail_system != 4) {
key = search.file
} else {
key = search.id || id;
}
}
if (typeof tree === 'object' && typeof tree.activateKey === 'function') {
tree.activateKey(key)
} else {
setTimeout(() => {
this.set(key);
}, 1e2);
}
}
/**
* Update mail folder unread counter
*
* @param {object} data Response object with data for current page
*
* @return {void}
*/
const update = function(data) {
let allowed = data.folder_counts_allowed,
unread_count = data.unread,
$node_titles = $($$.selector('tree.title')),
active_node = $$.selector('tree.active'),
node_bubble = $$.selector('tree.bubble'),
$active_node_title = $(active_node).find($$.selector('tree.title')),
$active_node_bubble = $(active_node).find(node_bubble);
// Update active folder counter
if (allowed) {
if (unread_count) {
if ($active_node_bubble.length) {
$active_node_bubble.text(unread_count)
} else {
$active_node_title.append($$.create.$('tree.bubble', false, 'span', unread_count))
}
} else {
$active_node_bubble.remove();
}
} else {
$node_titles.find(node_bubble).remove();
}
}
/**
* Adjust folders into view
*
* @return {void}
*/
const adjust = () => {
tree.adjust();
}
// Reveal sub-modules ;;
return {
get: get,
set: set,
update: update,
adjust: adjust,
data: data
}
})()
// Reveal modules (API) ;;
return {
folders: {
get: folders.get,
set: folders.set,
update: folders.update,
adjust: folders.adjust
},
messages: {
get: messages.get,
sort: messages.sort
}
}
})();