From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 547D18A8DA for ; Thu, 20 Oct 2022 21:15:51 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 35E211AB40 for ; Thu, 20 Oct 2022 21:15:21 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Thu, 20 Oct 2022 21:15:20 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id CC7C644AD9 for ; Thu, 20 Oct 2022 21:15:19 +0200 (CEST) From: Stoiko Ivanov To: pmg-devel@lists.proxmox.com Date: Thu, 20 Oct 2022 21:14:59 +0200 Message-Id: <20221020191500.2414-4-s.ivanov@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221020191500.2414-1-s.ivanov@proxmox.com> References: <20221020191500.2414-1-s.ivanov@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.175 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pmg-devel] [PATCH pmg-gui 3/4] quarantine: add common controller for all quarantines X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 20 Oct 2022 19:15:51 -0000 over the time the spam quarantine has gained quite a few nice usability improvments and features, which would be useful in the virus and attachment quarantines as well - e.g.: * multi-mail selection * keyboard actions * context menu * download mail as .eml by adding the common class (which is mostly a copy of the controller part of SpamQuarantine.js with changes of 'var' to 'let' and removal of the spam-specific parts), we can reuse it for all the quarantines. Signed-off-by: Stoiko Ivanov --- js/Makefile | 1 + js/controller/QuarantineController.js | 170 ++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 js/controller/QuarantineController.js diff --git a/js/Makefile b/js/Makefile index b904598..9a2bcf2 100644 --- a/js/Makefile +++ b/js/Makefile @@ -19,6 +19,7 @@ JSSRC= \ RuleInfo.js \ RuleEditor.js \ MainView.js \ + controller/QuarantineController.js \ QuarantineContextMenu.js \ QuarantineList.js \ SpamInfoGrid.js \ diff --git a/js/controller/QuarantineController.js b/js/controller/QuarantineController.js new file mode 100644 index 0000000..dfe2915 --- /dev/null +++ b/js/controller/QuarantineController.js @@ -0,0 +1,170 @@ +Ext.define('PMG.controller.QuarantineController', { + extend: 'Ext.app.ViewController', + xtype: 'controller.Quarantine', + alias: 'controller.quarantine', + + updatePreview: function(raw, rec) { + let preview = this.lookupReference('preview'); + + if (!rec || !rec.data || !rec.data.id) { + preview.update(''); + preview.setDisabled(true); + return; + } + + let url = `/api2/htmlmail/quarantine/content?id=${rec.data.id}`; + if (raw) { + url += '&raw=1'; + } + preview.setDisabled(false); + this.lookupReference('raw').setDisabled(false); + this.lookupReference('download').setDisabled(false); + preview.update(""); + }, + + multiSelect: function(selection) { + let me = this; + me.lookupReference('raw').setDisabled(true); + me.lookupReference('download').setDisabled(true); + me.lookupReference('mailinfo').setVisible(false); + + let preview = me.lookupReference('preview'); + preview.setDisabled(false); + preview.update(`

${gettext('Multiple E-Mails selected')} (${selection.length})

`); + }, + + toggleRaw: function(button) { + let me = this; + let list = me.lookupReference('list'); + let rec = list.selModel.getSelection()[0]; + me.lookupReference('mailinfo').setVisible(me.raw); + me.raw = !me.raw; + me.updatePreview(me.raw, rec); + }, + + btnHandler: function(button, e) { + let me = this; + let action = button.reference; + let list = me.lookupReference('list'); + let selected = list.getSelection(); + me.doAction(action, selected); + }, + + doAction: function(action, selected) { + if (!selected.length) { + return; + } + + let list = this.lookupReference('list'); + + if (selected.length > 1) { + let idlist = selected.map(item => item.data.id); + Ext.Msg.confirm( + gettext('Confirm'), + Ext.String.format( + gettext("Action '{0}' for '{1}' items"), + action, selected.length, + ), + async function(button) { + if (button !== 'yes') { + return; + } + + list.mask(gettext('Processing...'), 'x-mask-loading'); + + const sliceSize = 2500, maxInFlight = 2; + let batches = [], batchCount = Math.ceil(selected.length / sliceSize); + for (let i = 0; i * sliceSize < selected.length; i++) { + let sliceStart = i * sliceSize; + let sliceEnd = Math.min(sliceStart + sliceSize, selected.length); + batches.push( + PMG.Async.doQAction( + action, + idlist.slice(sliceStart, sliceEnd), + i + 1, + batchCount, + ), + ); + if (batches.length >= maxInFlight) { + await Promise.allSettled(batches); // eslint-disable-line no-await-in-loop + batches = []; + } + } + await Promise.allSettled(batches); // await possible remaining ones + list.unmask(); + // below can be slow, we could remove directly from the in-memory store, but + // with lots of elements and some failures we could be quite out of sync? + list.getController().load(); + }, + ); + return; + } + + PMG.Utils.doQuarantineAction(action, selected[0].data.id, function() { + let listController = list.getController(); + listController.allowPositionSave = false; + // success -> remove directly to avoid slow store reload for a single-element action + list.getStore().remove(selected[0]); + listController.restoreSavedSelection(); + listController.allowPositionSave = true; + }); + }, + + onSelectMail: function() { + let me = this; + let list = this.lookupReference('list'); + let selection = list.selModel.getSelection(); + if (selection.length > 1) { + me.multiSelect(selection); + return; + } + + let rec = selection[0] || {}; + + me.getViewModel().set('mailid', rec.data ? rec.data.id : ''); + me.updatePreview(me.raw || false, rec); + me.lookupReference('mailinfo').setVisible(!!rec.data && !me.raw); + me.lookupReference('mailinfo').update(rec.data); + }, + + openContextMenu: function(table, record, tr, index, event) { + event.stopEvent(); + let me = this; + let list = me.lookup('list'); + Ext.create('PMG.menu.QuarantineContextMenu', { + callback: action => me.doAction(action, list.getSelection()), + }).showAt(event.getXY()); + }, + + keyPress: function(table, record, item, index, event) { + let me = this; + let list = me.lookup('list'); + let key = event.getKey(); + let action = ''; + switch (key) { + case event.DELETE: + case 127: + action = 'delete'; + break; + case Ext.event.Event.D: + case Ext.event.Event.D + 32: + action = 'deliver'; + break; + } + + if (action !== '') { + me.doAction(action, list.getSelection()); + } + }, + + control: { + 'button[reference=raw]': { + click: 'toggleRaw', + }, + 'pmgQuarantineList': { + selectionChange: 'onSelectMail', + itemkeypress: 'keyPress', + rowcontextmenu: 'openContextMenu', + }, + }, +}); -- 2.30.2