From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <s.ivanov@proxmox.com>
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 <pmg-devel@lists.proxmox.com>; 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 <pmg-devel@lists.proxmox.com>; 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 <pmg-devel@lists.proxmox.com>; 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 <pmg-devel@lists.proxmox.com>; Thu, 20 Oct 2022 21:15:19 +0200 (CEST)
From: Stoiko Ivanov <s.ivanov@proxmox.com>
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
 <pmg-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pmg-devel>, 
 <mailto:pmg-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pmg-devel/>
List-Post: <mailto:pmg-devel@lists.proxmox.com>
List-Help: <mailto:pmg-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel>, 
 <mailto:pmg-devel-request@lists.proxmox.com?subject=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 <s.ivanov@proxmox.com>
---
 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("<iframe frameborder=0 width=100% height=100% sandbox='allow-same-origin' src='" + url +"'></iframe>");
+    },
+
+    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(`<h3 style="padding-left:5px;">${gettext('Multiple E-Mails selected')} (${selection.length})</h3>`);
+    },
+
+    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