From ba20db2e086aeaaffbd101f37c056b25dfb3d2d4 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 28 Nov 2020 17:41:48 +0100 Subject: [PATCH] [Web] Allow a user to choose notification categories (junk folder, rejected mail, both/all) + user ACL --- data/Dockerfiles/dovecot/quarantine_notify.py | 32 ++++---- data/web/edit.php | 30 ++++++- data/web/inc/functions.inc.php | 3 + data/web/inc/functions.mailbox.inc.php | 79 ++++++++++++++++++- data/web/inc/init_db.inc.php | 4 +- data/web/inc/vars.inc.php | 6 ++ data/web/js/site/mailbox.js | 10 ++- data/web/json_api.php | 3 + data/web/lang/lang.de.json | 10 +++ data/web/lang/lang.en.json | 10 +++ data/web/mailbox.php | 11 ++- data/web/user.php | 27 +++++++ docker-compose.yml | 2 +- 13 files changed, 204 insertions(+), 23 deletions(-) diff --git a/data/Dockerfiles/dovecot/quarantine_notify.py b/data/Dockerfiles/dovecot/quarantine_notify.py index b0ed29b9..ae0a42f2 100755 --- a/data/Dockerfiles/dovecot/quarantine_notify.py +++ b/data/Dockerfiles/dovecot/quarantine_notify.py @@ -58,8 +58,13 @@ def query_mysql(query, headers = True, update = False): cur.close() cnx.close() -def notify_rcpt(rcpt, msg_count, quarantine_acl): - meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f' % (rcpt, max_score)) +def notify_rcpt(rcpt, msg_count, quarantine_acl, category): + if category == "add_header": category = "add header" + meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) + print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) + if len(meta_query) == 0: + return + msg_count = len(meta_query) if r.get('Q_HTML'): try: template = Template(r.get('Q_HTML')) @@ -117,6 +122,11 @@ records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, for record in records: attrs = '' attrs_json = '' + time_trans = { + "hourly": 3600, + "daily": 86400, + "weekly": 604800 + } try: last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt'])) if last_notification > time_now: @@ -133,18 +143,8 @@ for record in records: else: # if it's bytes then decode and load it attrs = json.loads(attrs.decode('utf-8')) - if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly', 'never'): - print('Abnormal quarantine_notification value') + if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'): continue - if attrs['quarantine_notification'] == 'hourly': - if last_notification == 0 or (last_notification + 3600) < time_now: - print("Notifying %s: Considering %d new items in quarantine" % (record['rcpt'], record['counter'])) - notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl']) - elif attrs['quarantine_notification'] == 'daily': - if last_notification == 0 or (last_notification + 86400) < time_now: - print("Notifying %s: Considering %d new items in quarantine" % (record['rcpt'], record['counter'])) - notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl']) - elif attrs['quarantine_notification'] == 'weekly': - if last_notification == 0 or (last_notification + 604800) < time_now: - print("Notifying %s: Considering %d new items in quarantine" % (record['rcpt'], record['counter'])) - notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl']) + if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) < time_now: + print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification'])) + notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) diff --git a/data/web/edit.php b/data/web/edit.php index 10b85660..3a0bb04b 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -572,6 +572,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { $rl = ratelimit('get', 'mailbox', $mailbox); $pushover_data = pushover('get', $mailbox); $quarantine_notification = mailbox('get', 'quarantine_notification', $mailbox); + $quarantine_category = mailbox('get', 'quarantine_category', $mailbox); $get_tls_policy = mailbox('get', 'tls_policy', $mailbox); if (!empty($result)) { ?> @@ -660,7 +661,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
-

+
+ +
+
+ + + +
+

+
+
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 1181b3f5..21852259 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -332,6 +332,9 @@ function hasDomainAccess($username, $role, $domain) { } function hasMailboxObjectAccess($username, $role, $object) { global $pdo; + if (empty($username) || empty($role) || empty($object)) { + return false; + } if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { return false; } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 0ca2253b..518dee15 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -949,6 +949,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']); + $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']); $quota_b = ($quota_m * 1048576); $mailbox_attrs = json_encode( array( @@ -960,7 +961,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'pop3_access' => strval($pop3_access), 'smtp_access' => strval($smtp_access), 'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']), - 'quarantine_notification' => strval($quarantine_notification) + 'quarantine_notification' => strval($quarantine_notification), + 'quarantine_category' => strval($quarantine_category) ) ); if (!is_valid_domain_name($domain)) { @@ -1409,6 +1411,65 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; + case 'quarantine_category': + if (!is_array($_data['username'])) { + $usernames = array(); + $usernames[] = $_data['username']; + } + else { + $usernames = $_data['username']; + } + if (!isset($_SESSION['acl']['quarantine_category']) || $_SESSION['acl']['quarantine_category'] != "1" ) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + foreach ($usernames as $username) { + if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + $is_now = mailbox('get', 'quarantine_category', $username); + if (!empty($is_now)) { + $quarantine_category = (isset($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category']; + } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + if (!in_array($quarantine_category, array('add_header', 'reject', 'all'))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + $stmt = $pdo->prepare("UPDATE `mailbox` + SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', :quarantine_category) + WHERE `username` = :username"); + $stmt->execute(array( + ':quarantine_category' => $quarantine_category, + ':username' => $username + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('mailbox_modified', $username) + ); + } + break; case 'spam_score': if (!is_array($_data['username'])) { $usernames = array(); @@ -2803,6 +2864,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attrs = json_decode($attrs['attributes'], true); return $attrs['quarantine_notification']; break; + case 'quarantine_category': + $attrs = array(); + if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) { + if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { + return false; + } + } + else { + $_data = $_SESSION['mailcow_cc_username']; + } + $stmt = $pdo->prepare("SELECT `attributes` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array(':username' => $_data)); + $attrs = $stmt->fetch(PDO::FETCH_ASSOC); + $attrs = json_decode($attrs['attributes'], true); + return $attrs['quarantine_category']; + break; case 'filters': $filters = array(); if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) { diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 396e0aa5..8010bbdd 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "16112020_1210"; + $db_version = "28112020_1210"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -401,6 +401,7 @@ function init_db_schema() { "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", ), "keys" => array( @@ -1185,6 +1186,7 @@ function init_db_schema() { $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;"); foreach($tls_options as $tls_user => $tls_options) { $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in), `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out) diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 83b0e0bc..aa38a72f 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -167,6 +167,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['pop3_access'] = true; // Mailbox has SMTP access by default $MAILBOX_DEFAULT_ATTRIBUTES['smtp_access'] = true; +// Mailbox receives notifications about... +// "add_header" - mail that was put into the Junk folder +// "reject" - mail that was rejected +// "all" - mail that was rejected and put into the Junk folder +$MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category'] = 'reject'; + // Default mailbox format, should not be changed unless you know exactly, what you do, keep the trailing ":" // Check dovecot.conf for further changes (e.g. shared namespace) $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:'; diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 217a4323..68a5ad45 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -371,13 +371,14 @@ jQuery(function($){ ''; }}, {"name":"quarantine_notification","filterable": false,"title":lang.quarantine_notification,"breakpoints":"all"}, + {"name":"quarantine_category","filterable": false,"title":lang.quarantine_category,"breakpoints":"all"}, {"name":"in_use","filterable": false,"type":"html","title":lang.in_use,"sortValue": function(value){ return Number($(value).find(".progress-bar-mailbox").attr('aria-valuenow')); }, }, {"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"}, {"name":"rl","title":"RL","breakpoints":"all","style":{"width":"125px"}}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'✓':0==value&&'✕';}}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'✓':(0==value?'✕':2==value&&'—');}}, {"name":"action","filterable": false,"sortable": false,"style":{"min-width":"290px","text-align":"right"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} ], "empty": lang.empty, @@ -418,6 +419,13 @@ jQuery(function($){ } else if (item.attributes.quarantine_notification === 'weekly') { item.quarantine_notification = lang.weekly; } + if (item.attributes.quarantine_category === 'reject') { + item.quarantine_category = '' + lang.q_reject + ''; + } else if (item.attributes.quarantine_category === 'add_header') { + item.quarantine_category = '' + lang.q_add_header + ''; + } else if (item.attributes.quarantine_category === 'all') { + item.quarantine_category = lang.q_all; + } if (acl_data.login_as === 1) { item.action = '
' + ' ' + lang.edit + '' + diff --git a/data/web/json_api.php b/data/web/json_api.php index e552b025..783ba127 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1576,6 +1576,9 @@ if (isset($_GET['query'])) { case "quarantine_notification": process_edit_return(mailbox('edit', 'quarantine_notification', array_merge(array('username' => $items), $attr))); break; + case "quarantine_category": + process_edit_return(mailbox('edit', 'quarantine_category', array_merge(array('username' => $items), $attr))); + break; case "qitem": process_edit_return(quarantine('edit', array_merge(array('id' => $items), $attr))); break; diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json index 46223842..4ab9331c 100644 --- a/data/web/lang/lang.de.json +++ b/data/web/lang/lang.de.json @@ -14,6 +14,7 @@ "quarantine": "Quarantäne-Aktionen", "quarantine_attachments": "Anhänge aus Quarantäne", "quarantine_notification": "Ändern der Quarantäne-Benachrichtigung", + "quarantine_category": "Ändern der Quarantäne-Benachrichtigungskategorie", "ratelimit": "Rate limit", "recipient_maps": "Empfängerumschreibungen", "smtp_ip_access": "Verwalten der erlaubten Hosts für SMTP", @@ -691,7 +692,11 @@ "owner": "Besitzer", "private_comment": "Privater Kommentar", "public_comment": "Öffentlicher Kommentar", + "q_add_header": "Junk-Ordner", + "q_all": "Alle Kategorien", + "q_reject": "Abgelehnt", "quarantine_notification": "Quarantäne-Benachrichtigung", + "quarantine_category": "Quarantäne-Benachrichtigungskategorie", "quick_actions": "Aktionen", "recipient_map": "Empfängerumschreibung", "recipient_map_info": "Empfängerumschreibung ersetzen den Empfänger einer E-Mail vor dem Versand.", @@ -996,8 +1001,13 @@ "pushover_title": "Notification Titel", "pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.
Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie hängen nicht voneinander ab und werden der Reihe nach ausgeführt.
Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)", "pushover_verify": "Verbindung verifizieren", + "q_add_header": "Junk-Ordner", + "q_all": "Alle Kategorien", + "q_reject": "Abgelehnt", "quarantine_notification": "Quarantäne-Benachrichtigung", + "quarantine_category": "Quarantäne-Benachrichtigungskategorie", "quarantine_notification_info": "Wurde über eine E-Mail in Quarantäne informiert, wird sie als \"benachrichtigt\" markiert und keine weitere Benachrichtigung zu dieser E-Mail versendet.", + "quarantine_category_info": "Die Kategorie \"Abgelehnt\" informiert über abgelehnte E-Mails, während \"Junk-Ordner\" über E-Mails berichtet, die im Junk-Ordner des jeweiligen Benutzers abgelegt wurden.", "remove": "Entfernen", "running": "Wird ausgeführt", "save": "Änderungen speichern", diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json index 51825450..0452cfb3 100644 --- a/data/web/lang/lang.en.json +++ b/data/web/lang/lang.en.json @@ -14,6 +14,7 @@ "quarantine": "Quarantine actions", "quarantine_attachments": "Quarantine attachments", "quarantine_notification": "Change quarantine notifications", + "quarantine_category": "Change quarantine notification category", "ratelimit": "Rate limit", "recipient_maps": "Recipient maps", "smtp_ip_access": "Change allowed hosts for SMTP", @@ -691,7 +692,11 @@ "owner": "Owner", "private_comment": "Private comment", "public_comment": "Public comment", + "q_add_header": "Junk folder", + "q_all": "All categories", + "q_reject": "Rejected", "quarantine_notification": "Quarantine notifications", + "quarantine_category": "Quarantine notification category", "quick_actions": "Actions", "recipient_map": "Recipient map", "recipient_map_info": "Recipient maps are used to replace the destination address on a message before it is delivered.", @@ -997,8 +1002,13 @@ "pushover_title": "Notification title", "pushover_vars": "When no sender filter is defined, all mails will be considered.
Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.
Useable variables for text and title (please take note of data protection policies)", "pushover_verify": "Verify credentials", + "q_add_header": "Junk folder", + "q_all": "All categories", + "q_reject": "Rejected", "quarantine_notification": "Quarantine notifications", + "quarantine_category": "Quarantine notification category", "quarantine_notification_info": "Once a notification has been sent, items will be marked as \"notified\" and no further notifications will be sent for this particular item.", + "quarantine_category_info": "The notification category \"Rejected\" includes mail that was rejected, while \"Junk folder\" will notify a user about mails that were put into the junk folder.", "remove": "Remove", "running": "Running", "save": "Save changes", diff --git a/data/web/mailbox.php b/data/web/mailbox.php index 657d88e5..01663a44 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -116,7 +116,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
- +
@@ -144,6 +144,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
  • +
  • +
  • +
  • +
  • @@ -165,6 +169,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
    diff --git a/data/web/user.php b/data/web/user.php index 8cf804ba..57b2803f 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -337,6 +337,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
    :
    @@ -370,6 +371,32 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '

    +
    +
    :
    +
    +
    + + + +
    +

    +
    +

    diff --git a/docker-compose.yml b/docker-compose.yml index 16cd9bc9..bc63b5e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -194,7 +194,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.136 + image: mailcow/dovecot:1.137 depends_on: - mysql-mailcow dns: