php IHDR w Q )Ba pHYs sRGB gAMA a IDATxMk\U s&uo,mD )Xw+e?tw.oWp;QHZnw`gaiJ9̟灙a=nl[ ʨ G;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ w@H;@ q$ y H@E7j 1j+OFRg}ܫ;@Ea~ j`u'o> j- $_q?qS XzG'ay
files >> /usr/libexec/webmin/authentic-theme/extensions/mail/ |
files >> //usr/libexec/webmin/authentic-theme/extensions/mail/mail.src.js |
/*! * Authentic Theme (https://github.com/authentic-theme/authentic-theme) * Copyright Ilia Rostovtsev <ilia@virtualmin.com> * 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 'mailbox'; }, link: function() { let prefix = v___location_prefix; return prefix ? `${prefix}/${v___module}` : `/${this.name()}`; }, }, locale: { short: config_portable_theme_locale_format_short, } }, platform: { mac: window.navigator.platform === 'MacIntel', }, pjax: { fetch: get_pjax_content, }, fetch: { options: { headers: { 'x-requested-with': 'XMLHttpRequest' }, } }, load: load, sdata: get_server_data, mavailable: core.moduleAvailable, lang: theme_language, notification: { post: plugins.messenger.post, hideAll: plugins.messenger.toast.hideAll, }, file_chooser: plugins.chooser.file, button: { progress: snippets.progressive_button, lock: snippets.button_lock, }, rows: page_table_rows_control, document_title: theme_title_generate, update_mdata: core.updateModuleData, uri_param: uri_parse_param, error: connection_error, event: { generate: event_generate }, navigation: { reset: plugins.navigation.reset }, plugin: { json_to_query: Convert.json_to_query, serialized_to_json: Convert.serialized_to_json, nice_size: Convert.nice_size, html_escape: Convert.htmlEscape, html_strip: Convert.htmlStrip, quote_escape: Convert.quoteEscape, timestamp: snippets.datetime.locale, offset_adjust: page.handle.content.offset, preloader: { hide: page.handle.content.preloader.hide, }, moment: moment, 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: (target) => { let $target = target || $('[data-tooltip="mailbox"]'); $target.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 {string} */ 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} */ 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-lg btn-default fa fa-refresh-mdi' }, 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' } }, compose: { button: { inverse: 'btn-inverse', submit: 'btn-primary', schedule: 'btn-info', }, hidden: 'hidden', panel: { content: 'jsPanel-content', container: 'jspCompose', container_shown: 'jspShown', backdrop: 'compose_backdrop', }, editor: { compose: 'ql-compose', composer: 'data-composer', scheduled: 'scheduled', content: 'ql-editor', toolbar: 'ql-toolbar', disabled: 'ql-disabled', tb_bold: 'ql-bold', tb_link: 'ql-link', tb_image: 'ql-image', controls: { compose: 'compose-controls', more: 'more-options', extra: { attach: 'e-attachment', link: 'e-ql-link', image: 'e-ql-image', html: 'e-html', discard: 'e-discard', } } }, form: { header: 'form-head', recipients: { control: 'recipients-control', fields: 'recipients-control-fields', }, name: { tattach: 'tattachments', scheduled: 'scheduled', } }, icons: { upload: { server: 'fa fa-fw fa-download-cloud', attach: 'fa2 fa2-attach', } } }, notification: { danger: 'exclamation-triangle', error: 'exclamation-circle', success: 'check-circle', type: { search: 'search', scheduled: 'clock', trash: '- fa2 fa2-trash', } }, class: { events_none: 'pointer-events-none', }, /** * Returns templates * * @returns {string} */ template: { compose: (data) => { let hidden = ' class="' + data.class.hidden + '"', empty = String(), status = { server_file: empty, abook: empty, crypt: empty, sign: empty, dsn: empty, del: empty, menu: { server_file: empty, encrypt: empty, options: empty, }, }, value = { server_file: data.toggle.more.server_file, crypt: data.toggle.more.crypt[0], sign: data.toggle.more.sign[0], abook: data.toggle.more.abook, dsn: data.toggle.more.dsn, del: data.toggle.more.del, } value.server_file === null && (status.server_file = hidden); if (value.server_file === null) { status.menu.server_file = hidden } value.crypt === null && (status.crypt = hidden); value.sign === null && (status.sign = hidden); if (value.crypt === null && value.sign === null) { status.menu.encrypt = hidden } value.abook === null && (status.abook = hidden); value.dsn === null && (status.dsn = hidden); value.del === null && (status.del = hidden); if (value.abook === null && value.dsn === null && value.del === null) { status.menu.options = hidden } return ` <form class="compose" data-pjax="no" action="${data.prefix}/${data.target.send}?id=${data.id}" method="post" enctype="multipart/form-data" accept-charset="${data.charset}"> <div class="form-e"> <div class="${data.class.form.header}"> <div class="form-group from"> <div class="flex"> <div class="col-xs-1"> <label for="c-from-${data.id}">${data.language.real || data.language.from}</label> </div> <div class="col-xs-11"> <span class="btn-group ${data.class.form.recipients.control}"> <button type="button" class="btn btn-link btn-transparent-link btn-resized btn-link-bordered cc${data.toggle.recipients.cc}">Cc</button> <button type="button" class="btn btn-link btn-transparent-link btn-resized btn-link-bordered bcc${data.toggle.recipients.bcc}">Bcc</button> </span> ${typeof data.from === 'object' ? `<div class="input-group c-from-input-group"> <input type="text" name="real" id="c-from-${data.id}" value="${data.from.name}" placeholder="${data.language._name}"> <span class="ltgt"><</span><input type="text" name="user" value="${data.from.user}" placeholder="${data.language._username}"> <span class="input-group-addon">@${data.from.dom}></span> <input type="hidden" name="dom" value="${data.from.dom}"> </div>` : data.from } </div> </div> </div> <div class="form-group to"> <div class="flex"> <div class="col-xs-1"> <label for="c-to-${data.id}">${data.language.to}</label> </div> <div class="col-xs-11"> ${data.to} </div> </div> </div> <div class="${data.class.form.recipients.fields}"> <div class="form-group cc${data.toggle.recipients.ccf}"> <div class="flex"> <div class="col-xs-1"> <label for="c-cc-${data.id}">${data.language.cc}</label> </div> <div class="col-xs-11"> ${data.cc} </div> </div> </div> <div class="form-group bcc${data.toggle.recipients.bccf}"> <div class="flex"> <div class="col-xs-1"> <label for="c-bcc-${data.id}">${data.language.bcc}</label> </div> <div class="col-xs-11"> ${data.bcc} </div> </div> </div> </div> <div class="form-group"> <div class="flex"> <div class="col-xs-1"> <label for="c-subject-${data.id}">${data.language.subject}</label> </div> <div class="col-xs-11"> ${data.subject} </div> </div> </div> <div class="form-group"> <div class="flex attachments hidden"> <div class="col-xs-1"> <label for="c-attach-${data.id}">${data.language._attachments}</label> </div> <div class="col-xs-11"> ${data.attachments} </div> </div> </div> </div> <div class="compose-controls-block"> <div class="ql-compose-container"> <textarea data-signature="${data.signature}" class="${data.status.text}" ${data.class.editor.composer}="text">\n\n\n${data.signature}</textarea> <div ${data.class.editor.composer}="html" class="ql-compose ql-container-toolbar-bottom ${data.status.html}">${data.body}</div> <div id="tb-${data.id}"> <span class="ql-formats"> <select class="ql-font"> <option value="initial" selected>${data.language._default}</option> <option value="sans-serif">Sans Serif</option> <option value="serif">Serif</option> <option value="monospace">Monospace</option> </select> <select class="ql-size"> <option value="0.75em">${data.language._font_size.small}</option> <option selected>${data.language._font_size.normal}</option> <option value="1.2em">${data.language._font_size.medium}</option> <option value="1.5em">${data.language._font_size.large}</option> <option value="2.5em">${data.language._font_size.huge}</option> </select> </span> <span class="ql-formats"> <button class="ql-bold"></button> <button class="ql-italic"></button> <button class="ql-underline"></button> <select class="ql-color"></select> <select class="ql-background"></select> </span> <span class="ql-formats"> <select class="ql-align"></select> </span> <span class="ql-formats"> <button class="ql-list" value="ordered"></button> <button class="ql-list" value="bullet"></button> </span> <span class="ql-formats"> <span class="dropup"> <button class="btn btn-default dropdown-toggle pd-0" type="button" id="extra-${data.id}" data-toggle="dropdown" aria-expanded="true"> <span class="fa fa-lg fa-menu"></span> </button> <ul class="dropdown-menu pull-right" role="menu" aria-labelledby="extra-${data.id}"> <li role="presentation"><button role="menuitem" tabindex="-1" class="ql-strike"></button></li> <li role="presentation"><button role="menuitem" tabindex="-1" class="ql-blockquote"></button></li> <li role="presentation"><button role="menuitem" tabindex="-1" class="ql-code-block"></button></li> <li role="presentation" class="${data.class.hidden}"><button role="menuitem" tabindex="-1" class="ql-link"></button></li> <li role="presentation" class="${data.class.hidden}"><button role="menuitem" tabindex="-1" class="ql-image"></button></li> <li role="presentation"><button role="menuitem" tabindex="-1" class="ql-clean"></button></li> </ul> </span> </span> </div> </div> <div class="btn-group ${data.class.editor.controls.compose}"> <button type="submit" class="btn btn-primary btn-progress"> <span> <span>${data.language._send}</span> <span> <span class="progressing"></span> </span> </span> </button> <button type="button" class="btn btn-primary dropdown-toggle ${data.status.module.schedule}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="fa fa-0_90x fa-clock"></span> </button> <ul class="dropdown-menu ${data.class.editor.scheduled} ${data.status.module.schedule}"> <li><a>${$$.create.checkbox(0, 'scheduled', 1)}${data.language._scheduled}</a></li> </ul> <button type="button" class="btn btn-link btn-transparent-link ${data.class.editor.controls.extra.attach}" data-title="${data.language._attach}"><i class="fa-fw fa2 fa2-attach fa-md"></i></button> <button type="button" class="btn btn-link btn-transparent-link ${data.class.editor.controls.extra.link} ${data.status.html}" ${data.class.editor.composer}-h data-title="${data.language._insert_link}"><i class="fa-fw fa2 fa2-link fa-1_25x"></i></button> <button type="button" class="btn btn-link btn-transparent-link ${data.class.editor.controls.extra.image} ${data.status.html}" ${data.class.editor.composer}-h data-title="${data.language._insert_picture}"><i class="fa fa-fw fa-md fa-image"></i></button> <button type="button" class="btn btn-link btn-transparent-link ${data.class.editor.controls.extra.html}" data-title="${data.language._toggle}"><i class="fa fa-fw fa-md fa-html"></i></button> </div> <div class="btn-group ${data.class.editor.controls.compose} pull-right"> <span class="dropup ${data.class.editor.controls.more}"> <button class="btn btn-link btn-transparent-link dropdown-toggle" type="button" id="${data.class.editor.controls.more}-${data.id}" data-toggle="dropdown" aria-expanded="true"> <span class="fa fa-lg fa-dots-vertical"></span> </button> <ul class="dropdown-menu pull-right" role="menu" aria-labelledby="${data.class.editor.controls.more}-${data.id}"> <li${status.server_file} role="presentation"><a data-value="server-attach"><i class="fa fa-fw fa-download-cloud"></i> ${data.language._server_attach}</a></li> <li${status.menu.server_file} class="divider"></li> <li${status.menu.encrypt} class="dropdown-submenu right" role="menu"> <a tabindex="-1">${data.language._encrypt}</a> <ul class="dropdown-menu" role="menu" data-type="encrypt"> <li data-encrypt-container> <div class="menu-group"> <div${status.sign}> <label>${data.language.sign}</label> ${data.toggle.more.sign[1]} </div> <div${status.crypt}> <label>${data.language.crypt}</label> ${data.toggle.more.crypt[1]} </div> </div> </li> </ul> </li> <li class="dropdown-submenu right" role="menu"> <a tabindex="-1">${data.language.pri.label}</a> <ul class="dropdown-menu" role="menu" data-type="priority"> <li><a tabindex="-1" data-value="1">${data.language.pri.data[0]}</a></li> <li><a tabindex="-1" data-value="2">${data.language.pri.data[1]}</a></li> <li><a tabindex="-1"><i class="fa fa-fw fa-check pull-left"></i>${data.language.pri.data[2]}</a></li> <li><a tabindex="-1" data-value="4">${data.language.pri.data[3]}</a></li> <li><a tabindex="-1" data-value="5">${data.language.pri.data[4]}</a></li> </ul> </li> <li${status.menu.options} class="divider"></li> <li${status.menu.options} class="dropdown-submenu right" role="menu"> <a tabindex="-1">${data.language._options}</a> <ul class="dropdown-menu" role="menu" data-type="options"> <li${status.abook}><a tabindex="-1">${$$.create.checkbox(0, 'abook', 1, 0, value.abook)}${data.language._addrecipients}</a></li> <li${status.dsn}><a tabindex="-1">${$$.create.checkbox(0, 'dsn', 1, value.dsn)}${data.language._notifications_dsn}</a></li> <li${status.del}><a tabindex="-1">${$$.create.checkbox(0, 'del', 1, value.del)}${data.language._notifications_del}</a></li> </ul> </li> </ul> </span> <button type="button" class="btn btn-link btn-transparent-link ${data.class.editor.controls.extra.discard}" data-title="${data.language._discard}"><i class="fa fa2 fa-fw fa-sm fa2-trash"></i></button> </div> </div> </div> </form> `; } } }, /** * 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), id = name; if (typeof name === 'object') { id = name[1]; name = name[0]; } return '<input ' + attribute + ' type="' + type + '" name="' + name + '" id="' + id + '" placeholder="' + placeholder + '" value="' + value + '">'; }, /** * Generates textarea element * * @returns {string} */ textarea: function(name = String(), placeholder = String(), value = String(), attributes = String()) { let attribute = this._attributes(attributes), id = name; if (typeof name === 'object') { id = name[1]; name = name[0]; } return '<textarea ' + attribute + ' name="' + name + '" id="' + id + '" placeholder="' + placeholder + '">' + value + '</textarea>'; }, /** * 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 = ' ', checked = String()) { let attribute = this._attributes(attributes), checkbox = String(), id = name + '-' + Math.floor(Math.random() * 9e10); !label && (label = ' '); checked && (checked = 'checked') 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, cbfunc) { 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>'; if (typeof cbfunc === 'function') { dropdown = cbfunc(dropdown); } 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(); }, } /** * Imports configs */ const config = { d: {}, set: function(config) { this.d = config; } }, /** * Compose object sub-module ;; * * @since 19.40 * * @return {object} Reveals compose module API */ compose = (function() { let xtarget = {}; xtarget.send = 'send_mail.cgi'; xtarget.reply = 'reply_mail.cgi'; // Load dependencies _.load.bundle(['jquery.jspanel', 'quill'], 1); /** * Creates new compose message dialog * * @param {object} [form] Refers to form data in case of replied message * @param {boolean} [inline] Returns composer without panel * @param {object} [types] Sets composer type for reply all and forward * * @returns {string} */ const message = (form = false, inline = false, types = {}) => { let path = _.path.prefix, cmodule = _.variable.module.name(), prefix = `${path}/${cmodule}`; xtarget.getSize = `${path}/index.cgi/?xhr-get_size=1&xhr-get_size_nodir=1&xhr-get_size_path=`; xtarget.delete = `${prefix}/delete_mail.cgi?confirm=1&delete=1&noredirect=1`; xtarget.schedule = `${path}/schedule/save.cgi`; xtarget.addressBook = `${prefix}/export.cgi?fmt=csv&dup=0&incgr=1`; if (typeof form === 'object' && form.length) { form = $(form).serialize() + '&reply=1'; types.new = 0; } else { form = 'new=1'; types.new = 1; } // Check for message type if (types.reply_all) { form += '&rall=1'; } else if (types.forward) { form += '&forward=1'; } // Get reply form as provided fetch(`${prefix}/${xtarget.reply}?${form}`, _.fetch.options) .then(rs => { return rs.text(); }).then(rs => { // Reply data for further send mail let $form = $(rs).find(`[action*="${xtarget.send}"]`), generate = { timestamp: () => { return _.plugin.moment().valueOf() * 1e2 }, random: () => { return Math.floor(Math.random() * 9e14); } }, id = generate.timestamp(), form_data_lost = $form.find(':checkbox:not(:checked)').attr('value', '0').prop('checked', true).map(function() { return this.name }).get(), form_data = $form.serialize(), signature = $.trim(_.plugin.quote_escape(_.plugin.html_strip($(rs).find('textarea[name="body"]').text()))); if (form_data) { form_data = _.plugin.serialized_to_json(form_data); let // Object data for extracted fields data = { visible: {}, hidden: {}, }, classes = $$.$.compose, toggle = { // Toggle visibility of extra fields and its controllers recipients: (id, data, data_visible) => { if (typeof data === 'object') { let target = data[0], state = data[1], rcs = `.${classes.form.recipients.control}`, rcsf = `.${classes.form.recipients.fields}`, rc = target.querySelector(rcs), rf = target.querySelector(rcsf); rc.querySelector(`.${id}`).classList.toggle(classes.button.inverse, !state) rf.querySelector(`.${id}`).classList.toggle(classes.hidden, state) return; } else if (data === 'rc') { return !data_visible[id] ? String() : ` ${classes.button.inverse}` } else if (data === 'rf') { return data_visible[id] ? String() : ` ${classes.hidden}` } }, // Toggle visibility of attachment field attachments: (panel) => { let a = panel.querySelector(`[name="${classes.form.name.tattach}"]`), l = a.previousSibling.querySelectorAll('.tag').length; a.parentNode.parentNode.classList.toggle(classes.hidden, !l); adjust.contenteditable(panel); }, // Add backdrop for maximized panel backdrop: (panel, show) => { let body = $('body'), re_zi = 99999, compose_backdrop = classes.panel.backdrop; if (show) { panel[0].dataset.zIndex = panel[0].style.zIndex; panel[0].style.zIndex = re_zi + 1; panel[0].setAttribute('maximized', 1); body.append(`<div class="modal-backdrop fade2 in zi-${re_zi} ${compose_backdrop}"></div>`) } else { if (panel[0]) { panel[0].style.zIndex = panel[0].dataset.zIndex; panel[0].removeAttribute('maximized'); delete panel[0].dataset.zIndex; } body.find(`.modal-backdrop.${compose_backdrop}`).remove(); } }, // Toggle HTML/text input formatting: (target, status) => { let es = classes.editor.composer, eb = target.querySelectorAll(`[${es}-h]`), ed = target.querySelectorAll(`[${es}]`); eb.forEach((b) => { b.classList.toggle(classes.hidden, !status); }) ed.forEach((e) => { if (e.getAttribute(es) === 'text') { e.classList.toggle(classes.hidden, status) } else { e.classList.toggle(classes.hidden, !status) } }) adjust.contenteditable(target); }, }, adjust = { // Adjust the size of editable area contenteditable: (panel) => { let target = panel.querySelector(`.${classes.panel.content}`), container = target ? target.offsetHeight : window.innerHeight / 4, top_block = panel.querySelector(`.${classes.form.header}`).offsetHeight, editor_toolbar = panel.querySelector(`.${classes.editor.toolbar}`).offsetHeight, editor = panel.querySelector(`[${classes.editor.composer}]:not(.${classes.hidden})`), offset = 50 + editor_toolbar, height = `${container - top_block - offset}px`; editor.style.height = height; }, // Define modifier key modifier: (str) => { return str.replace(/%cmd/, _.platform.mac ? 'Cmd' : 'Ctrl'); } }, check = { field: (field, object) => { let value = object[field]; if (value && !isNaN(value)) { value = ~~value; } return typeof value === 'undefined' ? null : value }, }, element = { input: (str, data, readonly = false, no_escape = false, type = 'text') => { let value = (typeof data === 'object' ? data[str] : data); if (readonly) { readonly = ['readonly']; } if (!no_escape) { value = _.plugin.html_escape(value); } return $$.create.input([str, `c-${str}-${id}`], String(), value, type, readonly); }, select: {}, type: { time: () => { let ct = new Date(), format = (s) => { return ('0' + s).substr(-2) }, round = (m) => { let r = Math.ceil(m / 10) * 10; return r === 60 ? r - 5 : r; }, h = format(ct.getHours()), m = round(format(ct.getMinutes())); return `<input type="time" name="time" step="300" value="${h}:${m}">`; }, date: () => { let ct = new Date(), y = ct.getFullYear(), m = ct.getMonth() + 1, d = ct.getDate(); return `<input type="text" name="date" data-value="${y}-${m}-${d}">`; } }, composer: function(target) { let panel = target, paneled = panel.header ? true : false, config_html = { allowed: parseInt(data.hidden.html_edit), initial: parseInt(data.hidden.html_edit_config), }, config_update = function(option, value) { _.update_mdata("/uconfig.cgi?mailbox", "/uconfig_save.cgi", { [option]: value }) }, qs = Quill.import('attributors/style/size'), qf = Quill.import('attributors/style/font'); // Quill: assign font-size and font-family, rather than using classes qs.whitelist = ["0.75em", "1.2em", "1.5em", "2.5em"]; qf.whitelist = ["initial", "sans-serif", "serif", "monospace"]; Quill.register(qs, true); Quill.register(qf, true); // Redefine the actual target target = target[0]; let asb = target.querySelector(`.${classes.form.header}`), ccs = target.querySelectorAll(`.${classes.editor.controls.compose}`), rcs = target.querySelector(`.${classes.form.recipients.control}`), qtg = target.querySelector(`.${classes.editor.compose}`), tcm = target.querySelector(`[${classes.editor.composer}="text"]`), editor = { this: new Quill(qtg, { modules: { formula: false, syntax: false, imageDrop: true, toolbar: target.querySelector(`#tb-${id}`), }, bounds: target, theme: 'snow' }), get: { text: () => { return tcm.value }, html: () => { return editor.this.root.innerHTML }, data: () => { return config_html.allowed ? editor.get.html() : editor.get.text(); }, }, convert: () => { let he = editor.this, te = he.root.parentElement.previousElementSibling; if (config_html.allowed) { he.setText(te.value); } else { te.value = he.getText(); } }, maximized: () => { return target.hasAttribute('maximized'); } }, // Update message title dynamically title_update = function(ds) { let sf = asb.querySelector('[name="subject"]'), // Trigger title update ud = () => { sf.dispatchEvent(new Event('input')); }, // Change opacity for notifications us = (tg, df) => { if (paneled) { tg.style.opacity = (df ? 0.7 : 1); } }, // Display draft processing notifications du = (tg) => { if (ds === 1) { tg.textContent = _.lang('mail_composer_draft_saving'); us(tg, true); } else if (ds === -1) { tg.textContent = _.lang('mail_composer_draft_saved'); us(tg, true); // Change status back to original title setTimeout(() => { us(tg); ud(); }, 2e3) } } if (paneled) { let pt = panel.header.title[0], pti = pt.textContent; if (ds) { du(pt, pti); } else { sf.addEventListener('input', function() { pt.textContent = this.value || pti; }) // Update subject on initial load for replied mail ud(); } } }; paneled && target.classList.add(classes.panel.container, classes.panel.container_shown); adjust.contenteditable(target); // Reflect subject in panel title if exists title_update(); // Toggle HTML/text editor state let ctl_tgl = ccs[0].querySelector(`.${classes.editor.controls.extra.html}`); ctl_tgl.addEventListener('click', () => { let st = parseInt(config_html.allowed) || 0, ia = parseInt(config_html.initial) || 0, sg = +!st, co = sg ? 2 : (ia === 1 ? 1 : 0); toggle.formatting(target, sg); config_html.allowed = sg; // Change actual config option config_update('html_edit', co); // Convert current message to make sure HTML is removed editor.convert(); }) // Event to automatically adjust adjust real name and username let from_from = target.querySelector('input[name="from"]:not(disabled)'), from_name = target.querySelector('input[name="real"]'), from_user = target.querySelector('input[name="user"]'); if (from_name && from_user) { $.fn.eW = function(text, font) { if (!$.fn.eW.fakeEl) { $.fn.eW.fakeEl = $('<span data-eW>').hide().appendTo(document.body); } $.fn.eW.fakeEl.text(text || this.val() || this.text() || this.attr('placeholder')).css('font', font || this.css('font')); return $.fn.eW.fakeEl.width() + 7; }; [from_name, from_user].forEach((i, n) => { i.addEventListener('input', function() { $(this).css({ width: parseInt($(this).eW() + (!n && 3)) }) }); i.dispatchEvent(new Event('input')); }) } // Focus editable from input let from_focus = from_from || from_name; if (from_focus) { from_focus.focus(); from_focus.setSelectionRange(-1, -1); } // Register controls and its events setTimeout(() => { let tb = editor.this.options.modules.toolbar.container, upload_list = [], server_list = [], priority = null, server_attach_previous = null, attachments = target.querySelector(`[name="${classes.form.name.tattach}"]`), content = target.querySelector(`.${classes.editor.content}`), ctl_att = ccs[0].querySelector(`.${classes.editor.controls.extra.attach}`), ctl_lnk = ccs[0].querySelector(`.${classes.editor.controls.extra.link}`), ctl_img = ccs[0].querySelector(`.${classes.editor.controls.extra.image}`), ctl_dis = ccs[1].querySelector(`.${classes.editor.controls.extra.discard}`), submit = target.querySelector('button[type="submit"]'), to_ = target.querySelector('input[name="to"]'), cc_ = target.querySelector('input[name="cc"]'), bcc_ = target.querySelector('input[name="bcc"]'), $more_options = $(target).find(`.${classes.editor.controls.more}`), scheduled = { target: target.querySelector(`[name="${classes.form.name.scheduled}"]`), container: target.querySelector(`.${classes.editor.scheduled}`), events: function() { // Event to prevent closing dropdown for scheduled mail this.container.addEventListener('click', (event) => { event.stopPropagation(); }); // Event to change send/scheduled label for submit button this.checkbox().addEventListener('click', function() { let s = submit, t = s.querySelector('span').querySelector('span'), ct = _.lang('mail_composer_schedule'), c = this.checked, sb = classes.button.submit, sc = classes.button.schedule, d = s.nextElementSibling, st = language._send; s.classList.toggle(sc, c); s.classList.toggle(sb, !c); d.classList.toggle(sc, c); d.classList.toggle(sb, !c); t.textContent = c ? ct : st; }); // Initialize date picker this.datepicker(); }, status: function() { return this.target.checked; }, checkbox: function() { return this.container.querySelector('[type="checkbox"]'); }, holder: function() { return this.container.querySelector('[data-t]'); }, datepicker: function() { let tag = this.holder(), input = tag.previousSibling; // Event to handle change date for scheduled mail tag.addEventListener('click', function() { $(input).datepicker('show'); }); $(input).datepicker({ language: _.sdata("language"), todayHighlight: true, autoclose: true, startDate: "0d", }).on("changeDate", function(l) { let today = _.lang('global_today').toLowerCase(), tomorrow = _.lang('global_tomorrow').toLowerCase(), label = today, now = new Date(), y = now.getFullYear(), m = now.getMonth() + 1, d = now.getDate(), py = l.date.getFullYear(), pm = l.date.getMonth() + 1, pd = l.date.getDate(), date = l.dates[0], date_ = py + '-' + pm + '-' + pd, date_formatted = moment(date).format(_.variable.locale.short); this.dataset.value = date_; if ( y === py && m === pm && (d === pd || d + 1 === pd) ) { if (d + 1 === pd) { label = tomorrow } } else { label = date_formatted } tag.textContent = label }) } }, draft = { timeout: { update: null, discard: null, }, // Draft data data: [], // Reset draft data reset: function() { let folder = this.data[0]; this.data = []; if (folder) { this.data.push(folder); } }, // Test draft data test: function() { return this.data.length >= 1; }, // Initiate draft save save: function() { this.terminate(); this.timeout.update = setTimeout(() => { submit.dispatchEvent(new Event('click')); }, 2e3); }, // Terminate draft save terminate: function() { typeof this.timeout.update === 'number' && clearTimeout(this.timeout.update); }, // Discard the draft purge: function(id, folder, message) { fetch(`${xtarget.delete}&id=${id}&folder=${folder}&d=${message}`, _.fetch.options).then(r => { r.text().then(() => { draft.refresh(); }); }) }, // Update draft folder's content if opened refresh: function() { if (this.test() && folders.check(this.data[0])) { folders.refresh(); } }, clean: function() { this.test() && this.purge(this.data[0], this.data[1], this.data[3]); this.reset(); this.terminate(); }, control: { // Schedule draft for discard discard: function() { editor.maximized() && panel.normalize(); draft.timeout.discard = setTimeout(() => { draft.test() && draft.purge(draft.data[0], draft.data[1], draft.data[3]); draft.reset(); draft.terminate(); paneled && panel.close(); }, 5e3); }, // Undo discarded draft undo: function() { target.classList.remove(classes.hidden); typeof draft.timeout.discard === 'number' && clearTimeout(draft.timeout.discard); }, } }, // Process added attachment add_attachment = (type, id, filedata, size, update) => { let icon = (type === 'server' ? classes.icons.upload.server : classes.icons.upload.attach), name = filedata.name.split("/").pop() || filedata.name; $(attachments).tagsinput('add', `[i class="${icon}"][/i]${name} [em](${_.plugin.nice_size(size)})[/em]`); // Register reference for inserted tag let tags = attachments.previousSibling.querySelectorAll('.tag'), last = tags[tags.length - 1]; last.dataset.reference = id; // Store uploaded/attached file if (type === 'server') { server_list[id] = filedata.name; } else { upload_list[id] = filedata.file; } // Update editor on last if (update) { adjust.contenteditable(target) toggle.attachments(target); } }; // Event for external insert link to editor button ctl_lnk.addEventListener('click', () => { tb.querySelector(`.${classes.editor.tb_link}`).dispatchEvent(new Event('click')); }) // Event for external add images to editor button ctl_img.addEventListener('click', () => { tb.querySelector(`.${classes.editor.tb_image}`).dispatchEvent(new Event('click')); }) // Event for discarding the draft ctl_dis.addEventListener('click', () => { draft.control.discard(); let undo = { cancel: { label: _.lang('global_undo'), action: function() { draft.control.undo(); } } }; _.notification.post([$$.$.notification.type.trash, _.lang('mail_composer_discarded_draft')], 10, "warning", `discard-${id}`, 1, ['bottom', 'center'], undo); target.classList.add(classes.hidden); }); // Event for controlling visibility of extra recipients fields rcs.querySelectorAll('button').forEach((b) => { b.addEventListener('click', () => { let enabled = b.classList.contains(classes.button.inverse), type = b.classList.contains("bcc") ? 'bcc' : 'cc'; toggle.recipients(type, [target, enabled]); adjust.contenteditable(target); }); }) // Event for attaching new file(s) ctl_att.addEventListener('click', () => { let form = target.querySelector('form'), xu = document.createElement('input'); // Create temporary file input and listen for change xu.type = "file"; xu.setAttribute('multiple', 1); xu.classList.add(classes.hidden); xu = form.appendChild(xu); xu.click(); xu.addEventListener('change', function() { Array.from(this.files).forEach((file, i, arr) => { let fuid = generate.random() + i, size = file.size, name = file.name, last = (i === arr.length - 1); add_attachment('upload', fuid, { name: name, file: file }, size, last); last && xu.remove(); }) }) }) // Events to manage more options menu $more_options.find('.dropdown-menu').on("click.bs.dropdown", function(event) { let type = this.dataset.type, etarget = event.target, action = etarget.dataset.value; // Attach new file from server if (action === 'server-attach') { let error = { read: _.lang('mail_composer_server_attach_error_read'), dir: _.lang('mail_composer_server_attach_error_dir') }; _.file_chooser({ file: server_attach_previous }).then(file => { if (file) { let suid = generate.random(); fetch(xtarget.getSize + file, _.fetch.options).then(r => { r.text().then(rs => { let s = rs.split(`|`), size = s[1].replace(/\s+/g, String()); if (size == -1 || size == -2) { let message = size == -1 ? error.read : error.dir _.notification.post([$$.$.notification.danger, message], 10, "error", 0, 1, ['bottom', 'center']) } else { add_attachment('server', suid, { name: file }, size, true); } }); }) } server_attach_previous = file; }); return } // Prevent closing dropdown menu on click for all other event.stopPropagation(); // Change message priority if (type === 'priority') { let check = etarget.closest('ul').querySelector('i'); check.remove(); etarget.appendChild(check); priority = action ? ~~action : null; } // Toggle options checkboxes if (type === 'options') { let cb = etarget.querySelector('input[type="checkbox"]'); cb && (cb.checked ^= 1); } }); // Init attachment tags input $(attachments).tagsinput({ allowDuplicates: true, confirmKeys: [13], delimiter: '\\000', }); // Remove actual attachments upon removing a tag $(attachments).on('itemRemoved', (event) => { let item = event.item[1]; if (item) { delete upload_list[item]; delete server_list[item]; } toggle.attachments(target); }); // Init tooltip for compose controls _.plugin.tooltip($(ctl_att) .add(ctl_img) .add(ctl_att) .add(ctl_lnk) .add(ctl_tgl) .add(ctl_dis) ); // Create tooltip for editor controls let editor_controls = [ 'font', 'size', 'bold', 'italic', 'underline', 'color', 'background', 'align', { 'list': 'ordered' }, { 'list': 'bullet' }, 'strike', 'blockquote', 'code-block', 'link', 'clean', ] editor_controls.forEach((v) => { let button, key, value, language = 'editor_tb'; if (typeof v === 'object') { key = Object.keys(v)[0]; value = `${key}[value="${v[key]}"]`; language += `_${key}_${v[key]}`; } else { value = v; language += `_${v}`; } button = tb.querySelector(`.ql-${value}`) button.dataset.title = adjust.modifier(_.lang(language)); _.plugin.tooltip($(button)) }) // Event to handle change in header asb.addEventListener('input', function() { // Save the draft draft.save(); }) // Event to prevent default submit on input fields asb.querySelectorAll('input').forEach((input) => { input.addEventListener('keydown', (event) => { if (event.keyCode === 13) { event.preventDefault(); return } draft.save(); }); }) // Event to handle content change in HTML body editor.this.on('text-change', function() { // Save the draft draft.save(); }) // Event to handle content change in text body tcm.addEventListener('input', function() { // Save the draft draft.save(); }) // Scheduled mail events scheduled.events(); // Bring address book autocompletion fetch(xtarget.addressBook, _.fetch.options) .then(function(rs) { return rs.text(); }) .then(function(d) { [to_, cc_, bcc_].forEach(input => { // Bind tags input let tags = $(input).tagsinput({ confirmKeys: [13, 32], addOnBlur: false, cancelConfirmKeysOnEmpty: false, tagClass: 'label recipient' }); // Initialize autocomplete on received data, // if there is something in user's address book let a = _.lang('theme_xhred_global_alias'), b = d.match(/"(.*)","(.*)"/gm); if (b) { let book = []; b.map(function(en) { let gr = en.match(/"-","(.*)"/), em = en.match(/"(.*)","(.*)"/); if (gr) { book.push(a + " <" + em[2] + ">"); } else if (em) { book.push(em[2] + " <" + em[1] + ">"); } }); !$.isEmptyObject(book) && tags[0].$input.autocomplete({ lookup: book, autoSelectFirst: true, position: 'relative', appendTo: tags[0].$container, onSelect: function(m) { $(input).tagsinput('add', m.value); this.value = String(); } }); } $(input) .on('itemAdded itemRemoved', function(event) { // Soft validate email address let email = event.item, contact; if (email) { contact = email.match(/<(.*)>/); if (contact) { email = contact[1]; } if (!event.item.startsWith(a) && event.type === 'itemAdded' && !/.+@.+\..+/.test(email)) { $(event.target.previousSibling).find('.recipient').last().addClass('error') } } // Adjust the container size on adding/removing recipient adjust.contenteditable(target); }) // Imitate tab keypress to generate the tag as well tags[0].$input.on('keydown blur', function(event) { let value = this.value; if (event.keyCode === 9 || (event.type === 'blur' && event.relatedTarget)) { // Dispatch event to complete the tag $(this).trigger(_.event.generate('keypress', 32)); // Adjust the container size on adding/removing recipient adjust.contenteditable(target); if (value) { event.preventDefault(); } } }); }) }); // Submitting mail submit.addEventListener('click', function(event) { event.preventDefault(); let form = this.closest('form'), form_data = new FormData(form), trusted = event.isTrusted || ~~submit.dataset.isTrusted, draft_status = !trusted; // Reset trusted state for submit this.dataset.isTrusted = 0; // Terminate draft event in case mail is actually submitted if (trusted) { draft.terminate(); } // Add message body form_data.append('body', editor.get.data()); // Set message priority let pri_key = 'pri' priority ? form_data.set(pri_key, priority) : form_data.delete(pri_key); // Add hidden entries except ones already in the form Object.entries(data.hidden).forEach((e) => { let key = e[0], value = e[1]; if (!form_data.has(key)) { form_data.set(key, value) } }); // Add file uploads let fsus = Object.values(upload_list); fsus.length && fsus.forEach((f, i) => { form_data.set(`attach${i}`, f) }); // Add server attachments let ssus = Object.values(server_list); ssus.length && ssus.forEach((f, i) => { form_data.set(`file${i}`, f) }); // Update HTML/text mode status form_data.set('html_edit', config_html.allowed); // Force disable spellcheck for new mail composer form_data.set('spell', 0); // Check for draft draft_status && ( form_data.set('new', 0), form_data.set('enew', 1), form_data.set('save', 1), title_update(1) ); // Prepare scheduled mail let schedule = { date: { get: function(d) { let date = this.value, t = scheduled.container.querySelector('[name="date"]'); if (t) { date = t.dataset.value.split('-'); } return d === 'y' ? ~~date[0] : d === 'm' ? ~~date[1] : ~~date[2]; } }, time: { value: scheduled.container.querySelector('[type="time"]').value, get: function(t) { let time = ['12', '00']; if (this.value) { time = this.value.split(':'); } return t === 'h' ? ~~time[0] : ~~time[1]; } } } if (scheduled.status() && !draft_status) { let m = { body: 'mail', is_html: config_html.allowed, delete_after: 1, enabled: 1, status: 1, mode: 1, hour: schedule.time.get('h'), min: schedule.time.get('m'), day: schedule.date.get('d'), month: schedule.date.get('m'), year: schedule.date.get('y'), } // Extens submitted form data Object.entries(m).forEach(function(e, i) { if (i) { form_data.set(e[0], e[1]) } else { form_data.set(e[1], form_data.get(e[0])); form_data.delete(e[0]); } }) } // Post mail data let xhr = new XMLHttpRequest(), link = ((scheduled.status() && !draft_status) ? xtarget.schedule : form.getAttribute('action')); xhr.open("POST", link); xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest'); xhr.upload.onprogress = (e) => { !draft_status && ( _.button.progress(this, Math.ceil((e.loaded / e.total) * 100)), _.button.lock(this, true) ); }; xhr.onload = (e) => { let rs = e.target.responseText, status = String(), error = String(), error_container = false, parser = new DOMParser(), _g = function(param) { return _.uri_param(param, e.target.responseURL) }, _d = { id: _g('id'), folder: { index: _g('folder'), type: _g('folder_type'), id: _g('folder_id'), }, input: { id: form.querySelector('[name="id"]'), folder: form.querySelector('[name="folder"]'), }, }; // Handle previously saved draft if (draft_status) { // Update title title_update(-1); // Store reference for current draft draft.data = [_d.folder.id, _d.folder.index, (_d.input.id && _d.input.id.value), _d.id]; // Clear previous drafts for IMAP and POP if (_d.folder.type == 2 || _d.folder.type == 4) { if (_d.input.id) { draft.purge.apply(null, draft.data) } else { draft.refresh() } } else { // Refresh drafts folder at once when mail can be edited draft.refresh() } // Remove previously set data _d.input.id && _d.input.id.remove(); _d.input.folder && _d.input.folder.remove(); // Update form data form.insertAdjacentHTML('beforeend', element.input('id', _d.id, false, false, 'hidden')); form.insertAdjacentHTML('beforeend', element.input('folder', _d.folder.index, false, false, 'hidden')); } else { // Handle responses rs = parser.parseFromString(rs, 'text/html'); if (rs) { rs = rs.querySelector('.panel-body'), error_container = rs.querySelector('h3'); if (error_container) { // Send error notification error = error_container.innerHTML.replace(/\s:/, ': '); _.notification.post([$$.$.notification.danger, error], 10, "error", 0, 1, ['bottom', 'center']); // Reset progress _.button.progress(this, 0); // Unlock button _.button.lock(this, false) } else { // Send success notification status = rs.innerHTML; _.notification.post([scheduled.status() ? $$.$.notification.type.scheduled : $$.$.notification.success, status], 10, "success", 0, 1, ['bottom', 'center']) paneled && panel.close(); // Delete previously stored draft message draft.clean(); } } } } xhr.onerror = (e) => { // Reset progress _.button.progress(this, 0); // Unlock button _.button.lock(this, false) // Display error message _.error({ responseText: e.target.responseText, status: xhr.status }, 1); } xhr.send(form_data); }) // Submit mail using hotkey(%cmd-enter) target.addEventListener('keydown', e => { let meta = _.platform.mac ? e.metaKey : e.ctrlKey, enter = e.keyCode === 13; if (meta && enter) { submit.dataset.isTrusted = 1; submit.dispatchEvent(new Event('click')); } }); }, 3e2) }, }, language = {}, template = {}; // Group received fields Object.entries(form_data).filter((f) => { [ 'from', 'real', 'to', 'cc', 'bcc', 'subject', 'body' ].includes(f[0]) ? (data.visible[f[0]] = f[1]) : (data.hidden[f[0]] = f[1]); }); // Extract language strings for visible fields Object.entries(data.visible).forEach((e) => { let id = e[0]; language[id] = $form.find(`[name=${id}]`).parent().prev().text(); }); // Extract language strings for hidden fields Object.entries(data.hidden).forEach(function(e) { let id = e[0]; if (['crypt', 'sign'].includes(id)) { language[id] = $form.find(`[name=${id}]`).parent().prev().text(); } else if (['pri'].includes(id)) { let data = {}; $form.find(`[name=${id}] option`).map(function(ix) { data[ix] = this.innerText }); language[id] = { label: $form.find(`[name=${id}]`).parent().prev().text(), data: data }; } }); // Extend language with more strings language._attachments = _.lang('global_attachments'); language._send = _.lang('mail_composer_send'); language._scheduled = _.lang('mail_composer_scheduled') .replace(/%1/, `<span data-i>${element.type.date()}<span data-t>${_.lang('global_today').toLowerCase()}</span></span>`) .replace(/%2/, element.type.time()); language._attach = _.lang('mail_composer_attach'); language._insert_link = adjust.modifier(_.lang('editor_tb_link')); language._insert_picture = _.lang('mail_composer_insert_picture'); language._toggle = _.lang('mail_composer_toggle'); language._discard = _.lang('mail_composer_discard'); language._server_attach = _.lang('mail_composer_server_attach'); language._notifications = _.lang('global_notifications'); language._notifications_dsn = _.lang('mail_composer_notifications_dsn'); language._notifications_del = _.lang('mail_composer_notifications_del'); language._encrypt = _.lang('global_encrypt'); language._options = _.lang('global_options'); language._addrecipients = _.lang('mail_composer_addrecipients'); language._default = _.lang('global_default'); language._name = _.lang('mail_composer_real_name'); language._username = _.lang('mail_composer_username'); language._font_size = { small: _.lang('global_small'), normal: _.lang('global_normal'), medium: _.lang('global_medium'), large: _.lang('global_large'), huge: _.lang('global_huge'), }; // Check if we have composable from email address let from_name = $form[0].querySelector(`input[name="real"]`), from_user = $form[0].querySelector(`input[name="user"]`), from_dom = $form[0].querySelector(`input[name="dom"]`), from_composable; if (from_dom) { from_composable = { name: from_name.value, user: from_user.value, dom: from_dom.value, } } // Check for form selects element.select.from = $form[0].querySelector(`select[name="from"]`); element.select.sign = $form[0].querySelector(`select[name="sign"]`); element.select.crypt = $form[0].querySelector(`select[name="crypt"]`); if (element.select.from) { element.select.from = element.select.from.outerHTML; } if (element.select.sign) { element.select.sign = element.select.sign.outerHTML; } if (element.select.crypt) { element.select.crypt = element.select.crypt.outerHTML; } // Build mail form the template template.form = $$.$.template.compose({ prefix: prefix, target: { send: xtarget.send }, charset: data.hidden.charset, id: id, class: classes, language: language, status: { text: ~~data.hidden.html_edit ? classes.hidden : String(), html: ~~data.hidden.html_edit ? String() : classes.hidden, module: { schedule: _.mavailable('schedule') ? String() : classes.hidden, } }, toggle: { recipients: { cc: toggle.recipients('cc', 'rc', data.visible), bcc: toggle.recipients('bcc', 'rc', data.visible), ccf: toggle.recipients('cc', 'rf', data.visible), bccf: toggle.recipients('bcc', 'rf', data.visible), }, more: { server_file: check.field('file0', data.hidden), abook: check.field('abook', data.hidden), dsn: check.field('dsn', data.hidden), del: check.field('del', data.hidden), sign: [check.field('sign', data.hidden), element.select.sign], crypt: [check.field('crypt', data.hidden), element.select.crypt], pri: check.field('pri', data.hidden), } }, from: from_composable || element.select.from || element.input('from', data.visible, !~~config.d.g.edit_from), to: element.input('to', data.visible), cc: element.input('cc', data.visible), bcc: element.input('bcc', data.visible), subject: element.input('subject', data.visible), attachments: element.input(classes.form.name.tattach, data.visible, false, true), body: data.visible.body, signature: signature }) if (inline) { let inlne_form = inline.append(template.form); element.composer(inlne_form); } else { // Create compose panel let composers = $(`.${classes.panel.container} .${classes.editor.compose}`).length, window_width = window.innerWidth, small_window_width = window_width < 640, window_height = window.innerHeight, small_window_height = window_height < 640, small_window = small_window_width || small_window_height, ioffset = -15, offset = composers ? ioffset * 5 * composers : ioffset, position = small_window ? {} : { my: "right-bottom", at: "right-bottom", offsetX: offset, offsetY: offset }, panel = $.jsPanel({ position: position, theme: "dimgrey", onwindowresize: true, panelSize: { width: (small_window ? window_width + 4 * ioffset : 600), height: (small_window ? window_height + 4 * ioffset : 600) }, headerTitle: _.lang('mail_new_message'), content: template.form, maximizedMargin: { top: small_window ? -1 * ioffset : window_height * 0.03, bottom: small_window ? -1 * ioffset : window_height * 0.03, left: small_window ? -1 * ioffset : window_height * 0.1, right: small_window ? -1 * ioffset : window_height * 0.1, }, footerToolbar: function() {}, dblclicks: { title: "maximize" }, onminimized: function() { toggle.backdrop(this); }, onclosed: function() { toggle.backdrop(this); }, onnormalized: function() { adjust.contenteditable(this[0]); toggle.backdrop(this); }, onmaximized: function() { adjust.contenteditable(this[0]); toggle.backdrop(this, 1); }, callback: function() { element.composer(this); if (small_window) { this.maximize(); } }, }); panel.header.title.addClass('plain'); } } }); } // Reveal sub-modules ;; return { message: message, } })(); /** * 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() { /** * Control the state of previous fetch call */ const fetching = { state: false, initial: true, abort: function() { if (this.pending() === true && this.initial === false) { this.state.abort(); } }, pending: function() { if (typeof this.state.state === "function" && this.state.state() === "pending") { return true; } return false; }, }; /** * Fetches and renders list of messages for the given folder with particular pagination * * @returns {void} */ const get = (data) => { loader.start(); fetching.abort(); fetching.state = $.post(_.path.extensions + '/mail/messages.cgi?' + _.plugin.json_to_query(data), function(data) { render(data); loader.end(); _.document_title(0, _.lang('titles_mail')); // Set received config data config.set(data[0].config); fetching.initial = false; }); }, /** * 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 + ' ' + _.lang('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 ? _.lang('global_copy') : _.lang('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.post([$$.$.notification.danger, 'Forward functionality is no yet implemented. Expect it in the future beta pre-release.'], 10, "info", 0, 1, ['bottom', '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 (data && $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() { $$.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 = _.lang('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, _.fetch.options) .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() { compose.message(); }) }, /** * 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.hideAll()); $.post(target + hidden + actions + encodeURI(messages), function() { if (reset) { storage.reset(); } if (refetch || data.folder_counts_allowed) { fetching.abort(); $.post(_.path.extensions + '/mail/messages.cgi?' + hidden + 'show_body_len=' + preview_length() + '', function(data) { render(data); loader.end(); }); } }); }, /** * Render row * * @param {string} text * @param {string} icon * * @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.post([$$.$.notification.error, errors[i]], 20, "error", i, 1, ['bottom', 'center']); } // If redirect requested, follow it if (data.redirect) { fetching.abort(); _.pjax.fetch(data.redirect); } return } let messages_list_available = messages_list.length > 128 ? 1 : 0; if (!messages_list_available && data.searched) { _.notification.post([$$.$.notification.type.search, _.lang('mail_search_empty')], 5, "info", 0, 1, ['bottom', '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 ? _.lang('mail_pagination_last') : (data.pagination_arrow_first ? _.lang('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(), _.lang('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(), _.lang('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, _.lang('mail_' + dd + '') || _.lang('global_' + dd + '')) ) } } } } }); panel .find($$.selector('layout.column.6')).first() .append( $form_controls, $$.create.$('controls.refresh.button', { 'refresh': 1 }, 'button', String(), _.lang('global_refresh')), $$.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', _.lang('global_sort'), function(dd) { if (dd && dd.match(/<li.*?<a/)) { return dd; } return String(); }), $$.create.dropdown('controls.search.dropdown', [ [ $$.create.$(0, { [$$.$.controls.search.data.form.action]: 'search', [$$.$.controls.search.data.form.type]: 'simple' }, 'span', ( $$.create.input('search', _.lang('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', _.lang('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', _.lang('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', _.lang('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', _.lang('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', _.lang('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', _.lang('mail_search_with_status'))) + $$.create.$('layout.column.9', 0, 'span', $$.create.radio(0, 'status', -1, _.lang('mail_search_with_status_any'), 'status_def', 'checked') + $$.create.radio(0, 'status', 0, _.lang('mail_search_with_status_unread'), 'status0') + $$.create.radio(0, 'status', 1, _.lang('mail_search_with_status_read'), 'status1') + $$.create.radio(0, 'status', 2, _.lang('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', _.lang('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', _.lang('mail_search_limit_results'))) + $$.create.$('layout.column.9', 0, 'span', $$.create.radio(0, 'limit_def', 1, _.lang('global_no'), 'limit_def0', 'checked') + $$.create.radio(0, 'limit_def', 0, _.lang('mail_search_limit_results_yes') + ' ' + $$.create.input( 'limit', '', 20, 'number', ['step="10"', '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, _.lang('mail_search_has_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, _.lang('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()), _.lang('mail_search_clear')) : String() ), 'search', _.lang('global_search')), $$.create.$('controls.counter', 0, 'span') ) .end().last() .append( $$.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(); messages.refresh(panel); } else { events(); panel.append(row((data.folder_index === 0 ? _.lang('mail_no_new_mail') : _.lang('mail_no_mail')), 'messages.row.empty')); folders.update(data); } // Dismiss the loader for all calls setTimeout(() => { _.plugin.preloader.hide(); }, 2e2); }, /** * Set interval for automatic messages update * * @returns {void} */ refresh = function(panel, stop) { typeof this.refreshTimer === "number" && clearInterval(this.refreshTimer); // Clear timer and return if (stop) { return; } // Register last interaction time for smother UX let last_interaction_time = Date.now(); panel[0].addEventListener('mousemove', function() { last_interaction_time = Date.now(); }) // Update messages, if conditions are met this.refreshTimer = setInterval(() => { let refreshing = () => { !fetching.pending() && panel.find($$.element('controls.refresh.button')).trigger('click'); } if (config.d.u) { clearInterval(this.refreshTimer); if (config.d.u.refresh) { // Perform actual refresh this.refreshTimer = setInterval(() => { // Stop refresh if there is no mail list if (!document.querySelector(`.${panel[0].classList[0]}`)) { this.refresh(false, true); } // Refresh the page if user is not interacting with the page let is_active_search = $$.element('controls.search.clear.icon').length, is_checked = panel.find('[name="d"]:checked').length, is_pagination = panel.find('[href*="index.cgi"][href*="start=0"]').length, is_open = panel.find('.open').length; if (!is_checked && !is_pagination && !is_open && !is_active_search) { refreshing(); } }, parseInt(config.d.u.refresh) * 1000); } else { this.refresh(false, true); } } }, 1e2) } // Reveal sub-modules ;; return { get: get, storage: storage, refresh: refresh, events: events, } })(); /** * 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) => { if (!$.fn.fancytree) { setTimeout(() => { data.plugin.tree(source); }, 4e2); return; } let sourceTreeF = source === 'get' ? 'getTree' : null, sourceActiveNodeF = source === 'node' ? 'getActiveNode' : null; 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); _.pjax.fetch(data.url.link + encodeURIComponent(d.node.key)); messages.storage.reset(); _.navigation.reset(); } } }))); if ($(tree.container).length) { return sourceTreeF ? $.ui.fancytree.getTree($(tree.container)) : sourceActiveNodeF ? $.ui.fancytree.getTree($(tree.container)).getActiveNode() : $(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') + " " + _.lang('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(data.file.fancytree, 1, (_.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) mail.messages.events() } }); } /** * 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; // Set 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); } } /** * Refreshes mail in currently selected folder * * @returns {void} */ const refresh = () => { let node = tree.node(); node.span.click(); } /** * 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(); } } /** * Check if selected folder is what we are looking for * * @param {string} folder Folder name to check if currently selected * * @return {boolean} */ const check = function(folder) { let node = tree.node(); if (node && node.key === folder) { return true; } return false; } /** * Adjust folders into view * * @return {void} */ const adjust = () => { tree.adjust(); } // Reveal sub-modules ;; return { get: get, set: set, refresh: refresh, update: update, check: check, adjust: adjust, data: data } })(); // Reveal modules (API) ;; return { folders: { get: folders.get, set: folders.set, refresh: folders.refresh, update: folders.update, check: folders.check, adjust: folders.adjust }, messages: { get: messages.get, sort: messages.sort, events: messages.events, }, compose: compose.message } })();y~or5J={Eeu磝Qk ᯘG{?+]ן?wM3X^歌>{7پK>on\jy Rg/=fOroNVv~Y+ NGuÝHWyw[eQʨSb> >}Gmx[o[<{Ϯ_qFvM IENDB`