public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
Add Domain and Mailbox tagging (#4569)
* [Web] define tag tables * [Web] add mailbox tag functions * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * Include new tags lang in language.en.json * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging * [Web] add domain/mailbox tagging Co-authored-by: Niklas Meyer <[email protected]>
549ff7d1
data/conf/nginx/includes/site-defaults.conf | 2 +-
data/web/api/openapi.yaml | 154 ++++++++++++++
data/web/css/build/008-mailcow.css | 37 ++++
data/web/edit.php | 2 +
data/web/inc/functions.mailbox.inc.php | 300 ++++++++++++++++++++++++++--
data/web/inc/init_db.inc.php | 100 +++++++---
data/web/inc/vars.inc.php | 3 +
data/web/js/build/012-api.js | 17 +-
data/web/js/build/014-mailcow.js | 48 ++++-
data/web/js/site/mailbox.js | 19 +-
data/web/json_api.php | 71 +++++--
data/web/lang/lang.en.json | 1 +
data/web/templates/edit/domain.twig | 16 ++
data/web/templates/edit/mailbox.twig | 16 ++
data/web/templates/modals/mailbox.twig | 26 ++-
15 files changed, 731 insertions(+), 81 deletions(-)
Diff
diff --git a/data/conf/nginx/includes/site-defaults.conf b/data/conf/nginx/includes/site-defaults.conf
index f618c110..ef2c4fb8 100644
--- a/data/conf/nginx/includes/site-defaults.conf
+++ b/data/conf/nginx/includes/site-defaults.conf
@@ -65,7 +65,7 @@
}
location ~ ^/api/v1/(.*)$ {
- try_files $uri $uri/ /json_api.php?query=$1;
+ try_files $uri $uri/ /json_api.php?query=$1&$args;
}
location ^~ /.well-known/acme-challenge/ {
diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml
index 834b8589..b864f1b7 100644
--- a/data/web/api/openapi.yaml
+++ b/data/web/api/openapi.yaml
@@ -497,6 +497,7 @@ paths:
relay_all_recipients: "0"
rl_frame: s
rl_value: "10"
+ tags: ["tag1", "tag2"]
- null
msg:
- domain_added
@@ -544,6 +545,7 @@ paths:
rl_frame: s
rl_value: "10"
restart_sogo: "10"
+ tags: ["tag1", "tag2"]
properties:
active:
description: is domain active or not
@@ -1010,6 +1012,7 @@ paths:
force_pw_update: "1"
tls_enforce_in: "1"
tls_enforce_out: "1"
+ tags: ["tag1", "tag2"]
- null
msg:
- mailbox_added
@@ -1054,6 +1057,7 @@ paths:
force_pw_update: "1"
tls_enforce_in: "1"
tls_enforce_out: "1"
+ tags: ["tag1", "tag2"]
properties:
active:
description: is mailbox active or not
@@ -2716,6 +2720,140 @@ paths:
type: object
type: object
summary: Delete Transport Maps
+ "/api/v1/delete/mailbox/tag/{mailbox}":
+ post:
+ parameters:
+ - description: name of mailbox
+ in: path
+ name: mailbox
+ example: [email protected]
+ required: true
+ schema:
+ type: string
+ responses:
+ "401":
+ $ref: "#/components/responses/Unauthorized"
+ "200":
+ content:
+ application/json:
+ examples:
+ response:
+ value:
+ - log:
+ - mailbox
+ - delete
+ - tags_mailbox
+ - tags:
+ - tag1
+ - tag2
+ mailbox: [email protected]
+ - null
+ msg:
+ - mailbox_modified
+ - [email protected]
+ type: success
+ schema:
+ properties:
+ log:
+ description: contains request object
+ items: {}
+ type: array
+ msg:
+ items: {}
+ type: array
+ type:
+ enum:
+ - success
+ - danger
+ - error
+ type: string
+ type: object
+ description: OK
+ headers: {}
+ tags:
+ - Mailboxes
+ description: You can delete one or more mailbox tags.
+ operationId: Delete mailbox tags
+ requestBody:
+ content:
+ application/json:
+ schema:
+ example:
+ - tag1
+ - tag2
+ properties:
+ items:
+ description: contains list of mailboxes you want to delete
+ type: object
+ type: object
+ summary: Delete mailbox tags
+ "/api/v1/delete/domain/tag/{domain}":
+ post:
+ parameters:
+ - description: name of domain
+ in: path
+ name: domain
+ example: domain.tld
+ required: true
+ schema:
+ type: string
+ responses:
+ "401":
+ $ref: "#/components/responses/Unauthorized"
+ "200":
+ content:
+ application/json:
+ examples:
+ response:
+ value:
+ - log:
+ - mailbox
+ - delete
+ - tags_domain
+ - tags:
+ - tag1
+ - tag2
+ domain: domain.tld
+ - null
+ msg:
+ - domain_modified
+ - domain.tld
+ type: success
+ schema:
+ properties:
+ log:
+ description: contains request object
+ items: {}
+ type: array
+ msg:
+ items: {}
+ type: array
+ type:
+ enum:
+ - success
+ - danger
+ - error
+ type: string
+ type: object
+ description: OK
+ headers: {}
+ tags:
+ - Domains
+ description: You can delete one or more domain tags.
+ operationId: Delete domain tags
+ requestBody:
+ content:
+ application/json:
+ schema:
+ example:
+ - tag1
+ - tag2
+ properties:
+ items:
+ description: contains list of domains you want to delete
+ type: object
+ type: object
+ summary: Delete domain tags
/api/v1/edit/alias:
post:
responses:
@@ -2865,6 +3003,7 @@ paths:
quota: "10240"
relay_all_recipients: "0"
relayhost: "2"
+ tags: ["tag3", "tag4"]
items: domain.tld
properties:
attr:
@@ -3019,6 +3158,7 @@ paths:
sogo_access: "1"
username:
- [email protected]
+ tags: ["tag3", "tag4"]
- null
msg:
- mailbox_modified
@@ -3066,6 +3206,7 @@ paths:
- domain3.tld
- "*"
sogo_access: "1"
+ tags: ["tag3", "tag4"]
items:
- [email protected]
properties:
@@ -3793,6 +3934,11 @@ paths:
- all
- mailcow.tld
type: string
+ - description: comma seperated list of tags to filter by
+ example: "tag1,tag2"
+ in: query
+ name: tags
+ required: false
- description: e.g. api-key-string
example: api-key-string
in: header
@@ -3831,6 +3977,7 @@ paths:
relay_all_recipients: "0"
relayhost: "0"
rl: false
+ tags: ["tag1", "tag2"]
- active: "1"
aliases_in_domain: 0
aliases_left: 400
@@ -3853,6 +4000,7 @@ paths:
relay_all_recipients: "0"
relayhost: "0"
rl: false
+ tags: ["tag3", "tag4"]
description: OK
headers: {}
tags:
@@ -4345,6 +4493,11 @@ paths:
- all
- [email protected]
type: string
+ - description: comma seperated list of tags to filter by
+ example: "tag1,tag2"
+ in: query
+ name: tags
+ required: false
- description: e.g. api-key-string
example: api-key-string
in: header
@@ -4382,6 +4535,7 @@ paths:
rl: false
spam_aliases: 0
username: [email protected]
+ tags: ["tag1", "tag2"]
description: OK
headers: {}
tags:
diff --git a/data/web/css/build/008-mailcow.css b/data/web/css/build/008-mailcow.css
index d04feb25..75b96840 100644
--- a/data/web/css/build/008-mailcow.css
+++ b/data/web/css/build/008-mailcow.css
@@ -256,3 +256,40 @@ code {
.flag-icon {
margin-right: 5px;
}
+
+.tag-box {
+ display: flex;
+ flex-wrap: wrap;
+ height: auto;
+}
+.tag-badge {
+ transition: 200ms linear;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-left: 2px;
+ margin-right: 2px;
+}
+.tag-badge.btn-badge {
+ cursor: pointer;
+}
+.tag-badge .bi {
+ font-size: 12px;
+}
+.tag-badge.btn-badge:hover {
+ filter: brightness(0.9);
+}
+.tag-input {
+ margin-left: 10px;
+ border: 0;
+ flex: 1;
+ height: 24px;
+ min-width: 150px;
+}
+.tag-input:focus {
+ outline: none;
+}
+.tag-add {
+ padding: 0 5px 0 5px;
+ align-items: center;
+ display: inline-flex;
+}
\ No newline at end of file
diff --git a/data/web/edit.php b/data/web/edit.php
index dfba8479..8f9dcf3b 100644
--- a/data/web/edit.php
+++ b/data/web/edit.php
@@ -54,6 +54,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'rl' => $rl,
'rlyhosts' => $rlyhosts,
'dkim' => dkim('details', $domain),
+ 'domain_details' => $result,
];
}
elseif (isset($_GET['oauth2client']) &&
@@ -99,6 +100,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'rlyhosts' => $rlyhosts,
'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
'user_acls' => acl('get', 'user', $mailbox),
+ 'mailbox_details' => $result
];
}
elseif (isset($_GET['relayhost']) && is_numeric($_GET["relayhost"]) && !empty($_GET["relayhost"])) {
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index dccdcd93..f619f05d 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -443,16 +443,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
'msg' => 'access_denied'
);
return false;
}
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description'];
- if (empty($description)) {
- $description = $domain;
- }
+ if (empty($description)) $description = $domain;
+ $tags = (array)$_data['tags'];
$aliases = (int)$_data['aliases'];
$mailboxes = (int)$_data['mailboxes'];
$defquota = (int)$_data['defquota'];
@@ -545,10 +544,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
+
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain");
$stmt->execute(array(
':domain' => '%@' . $domain
));
+ // save domain
$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`)
VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients)");
$stmt->execute(array(
@@ -565,6 +566,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':relay_unknown_only' => $relay_unknown_only,
':relay_all_recipients' => $relay_all_recipients
));
+ // save tags
+ foreach($tags as $index => $tag){
+ if ($index > $GLOBALS['TAGGING_LIMIT']) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT'])
+ );
+ break;
+ }
+ $stmt = $pdo->prepare("INSERT INTO `tags_domain` (`domain`, `tag_name`) VALUES (:domain, :tag_name)");
+ $stmt->execute(array(
+ ':domain' => $domain,
+ ':tag_name' => $tag,
+ ));
+ }
+
try {
$redis->hSet('DOMAIN_MAP', $domain, 1);
}
@@ -942,6 +960,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$password = $_data['password'];
$password2 = $_data['password2'];
$name = ltrim(rtrim($_data['name'], '>'), '<');
+ $tags = $_data['tags'];
$quota_m = intval($_data['quota']);
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
$_SESSION['return'][] = array(
@@ -1103,6 +1122,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
+ // save tags
+ foreach($tags as $index => $tag){
+ if ($index > $GLOBALS['TAGGING_LIMIT']) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT'])
+ );
+ break;
+ }
+ $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':tag_name' => $tag,
+ ));
+ }
$stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';");
$stmt->execute(array(':username' => $username));
@@ -2146,6 +2181,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
$description = (!empty($_data['description']) && isset($_SESSION['acl']['domain_desc']) && $_SESSION['acl']['domain_desc'] == "1") ? $_data['description'] : $is_now['description'];
(int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['domain_relayhost']) && $_SESSION['acl']['domain_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['relayhost']);
+ $tags = (is_array($_data['tags']) ? $_data['tags'] : array());
}
else {
$_SESSION['return'][] = array(
@@ -2155,6 +2191,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
+
$stmt = $pdo->prepare("UPDATE `domain` SET
`description` = :description,
`gal` = :gal
@@ -2164,6 +2201,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':gal' => $gal,
':domain' => $domain
));
+ // save tags, tag_name is unique
+ foreach($tags as $index => $tag){
+ if ($index > $GLOBALS['TAGGING_LIMIT']) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT'])
+ );
+ break;
+ }
+ $stmt = $pdo->prepare("INSERT INTO `tags_domain` (`domain`, `tag_name`) VALUES (:domain, :tag_name)");
+ $stmt->execute(array(
+ ':domain' => $domain,
+ ':tag_name' => $tag,
+ ));
+ }
+
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2185,6 +2239,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$maxquota = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
$quota = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
$description = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
+ $tags = (is_array($_data['tags']) ? $_data['tags'] : array());
if ($relay_all_recipients == '1') {
$backupmx = '1';
}
@@ -2283,6 +2338,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
+
$stmt = $pdo->prepare("UPDATE `domain` SET
`relay_all_recipients` = :relay_all_recipients,
`relay_unknown_only` = :relay_unknown_only,
@@ -2312,6 +2368,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':description' => $description,
':domain' => $domain
));
+ // save tags, tag_name is unique
+ foreach($tags as $index => $tag){
+ if ($index > $GLOBALS['TAGGING_LIMIT']) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT'])
+ );
+ break;
+ }
+ $stmt = $pdo->prepare("INSERT INTO `tags_domain` (`domain`, `tag_name`) VALUES (:domain, :tag_name)");
+ $stmt->execute(array(
+ ':domain' => $domain,
+ ':tag_name' => $tag,
+ ));
+ }
+
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2360,6 +2433,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$quota_b = $quota_m * 1048576;
$password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
+ $tags = (is_array($_data['tags']) ? $_data['tags'] : array());
}
else {
$_SESSION['return'][] = array(
@@ -2636,6 +2710,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':relayhost' => $relayhost,
':username' => $username
));
+ // save tags
+ foreach($tags as $index => $tag){
+ if ($index > $GLOBALS['TAGGING_LIMIT']) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT'])
+ );
+ break;
+ }
+ $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':tag_name' => $tag,
+ ));
+ }
+
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2851,10 +2942,34 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
break;
case 'mailboxes':
$mailboxes = array();
- if (isset($_data) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
- return false;
+ if (isset($_extra) && is_array($_extra) && isset($_data)) {
+ // get by domain and tags
+ $tags = is_array($_extra) ? $_extra : array();
+
+ $sql = "";
+ foreach ($tags as $key => $tag) {
+ $sql = $sql."SELECT DISTINCT `username` FROM `tags_mailbox` WHERE `username` LIKE ? AND `tag_name` LIKE ?"; // distinct, avoid duplicates
+ if ($key === array_key_last($tags)) break;
+ $sql = $sql.' UNION DISTINCT '; // combine querys with union - distinct, avoid duplicates
+ }
+
+ // prepend domain to array
+ $params = array();
+ foreach ($tags as $key => $val){
+ array_push($params, '%'.$_data.'%');
+ array_push($params, '%'.$val.'%');
+ }
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($params);
+
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1]))
+ $mailboxes[] = $row['username'];
+ }
}
elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+ // get by domain
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
@@ -3348,20 +3463,46 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
return false;
}
- $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
- WHERE (`domain` IN (
- SELECT `domain` from `domain_admins`
- WHERE (`active`='1' AND `username` = :username))
- )
- OR 'admin'= :role");
- $stmt->execute(array(
- ':username' => $_SESSION['mailcow_cc_username'],
- ':role' => $_SESSION['mailcow_cc_role'],
- ));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $domains[] = $row['domain'];
+
+ if (isset($_extra) && is_array($_extra)){
+ // get by tags
+ $tags = is_array($_extra) ? $_extra : array();
+ // add % as prefix and suffix to every element for relative searching
+ $tags = array_map(function($x){ return '%'.$x.'%'; }, $tags);
+ $sql = "";
+ foreach ($tags as $key => $tag) {
+ $sql = $sql."SELECT DISTINCT `domain` FROM `tags_domain` WHERE `tag_name` LIKE ?"; // distinct, avoid duplicates
+ if ($key === array_key_last($tags)) break;
+ $sql = $sql.' UNION DISTINCT '; // combine querys with union - distinct, avoid duplicates
+ }
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($tags);
+
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ if ($_SESSION['mailcow_cc_role'] == "admin")
+ $domains[] = $row['domain'];
+ elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain']))
+ $domains[] = $row['domain'];
+ }
+ } else {
+ // get all
+ $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+ WHERE (`domain` IN (
+ SELECT `domain` from `domain_admins`
+ WHERE (`active`='1' AND `username` = :username))
+ )
+ OR 'admin'= :role");
+ $stmt->execute(array(
+ ':username' => $_SESSION['mailcow_cc_username'],
+ ':role' => $_SESSION['mailcow_cc_role'],
+ ));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $domains[] = $row['domain'];
+ }
}
+
return $domains;
break;
case 'domain_details':
@@ -3478,6 +3619,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$domain_admins = $stmt->fetch(PDO::FETCH_ASSOC);
(isset($domain_admins['domain_admins'])) ? $domaindata['domain_admins'] = $domain_admins['domain_admins'] : $domaindata['domain_admins'] = "-";
}
+ $stmt = $pdo->prepare("SELECT `tag_name`
+ FROM `tags_domain` WHERE `domain`= :domain");
+ $stmt->execute(array(
+ ':domain' => $_data
+ ));
+ $tags = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($tag = array_shift($tags)) {
+ $domaindata['tags'][] = $tag['tag_name'];
+ }
+
return $domaindata;
break;
case 'mailbox_details':
@@ -3613,6 +3764,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
$mailboxdata['is_relayed'] = $row['backupmx'];
}
+ $stmt = $pdo->prepare("SELECT `tag_name`
+ FROM `tags_mailbox` WHERE `username`= :username");
+ $stmt->execute(array(
+ ':username' => $_data
+ ));
+ $tags = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($tag = array_shift($tags)) {
+ $mailboxdata['tags'][] = $tag['tag_name'];
+ }
return $mailboxdata;
break;
@@ -4342,6 +4502,108 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
}
break;
+ case 'tags_domain':
+ if (!is_array($_data['domain'])) {
+ $domains = array();
+ $domains[] = $_data['domain'];
+ }
+ else {
+ $domains = $_data['domain'];
+ }
+ $tags = $_data['tags'];
+ if (!is_array($tags)) $tags = array();
+
+
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ $wasModified = false;
+ foreach ($domains as $domain) {
+ if (!is_valid_domain_name($domain)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => 'domain_invalid'
+ );
+ continue;
+ }
+
+ foreach($tags as $tag){
+ // delete tag
+ $wasModified = true;
+ $stmt = $pdo->prepare("DELETE FROM `tags_domain` WHERE `domain` = :domain AND `tag_name` = :tag_name");
+ $stmt->execute(array(
+ ':domain' => $domain,
+ ':tag_name' => $tag,
+ ));
+ }
+ }
+
+ if (!$wasModified) return false;
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('domain_modified', $domain)
+ );
+ break;
+ case 'tags_mailbox':
+ if (!is_array($_data['username'])) {
+ $usernames = array();
+ $usernames[] = $_data['username'];
+ }
+ else {
+ $usernames = $_data['username'];
+ }
+ $tags = $_data['tags'];
+ if (!is_array($tags)) $tags = array();
+
+ $wasModified = false;
+ foreach ($usernames as $username) {
+ if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => 'email invalid'
+ );
+ continue;
+ }
+
+ $is_now = mailbox('get', 'mailbox_details', $username);
+ $domain = $is_now['domain'];
+ if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => 'access_denied'
+ );
+ continue;
+ }
+
+ // delete tags
+ foreach($tags as $tag){
+ $wasModified = true;
+
+ $stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':tag_name' => $tag,
+ ));
+ }
+ }
+
+ if (!$wasModified) return false;
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => array('mailbox_modified', $username)
+ );
+ break;
}
break;
}
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 8e8a63b5..225e2baf 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 = "22032022_1330";
+ $db_version = "02052022_1500";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -23,35 +23,35 @@ function init_db_schema() {
}
$views = array(
- "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS
- SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias
- WHERE address!=goto
- AND active = '1'
- AND sogo_visible = '1'
- AND address NOT LIKE '@%'
- GROUP BY goto;",
- // START
- // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this
- // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X
- "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS
- SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl
- WHERE send_as NOT LIKE '@%'
- GROUP BY logged_in_as;",
- // END
- "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS
- SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl
- WHERE send_as NOT LIKE '@%' AND external = '1'
- GROUP BY logged_in_as;",
- "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
- SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
- LEFT OUTER JOIN alias_domain ON target_domain=domain
- GROUP BY username;",
- "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
- SELECT md5(script_data), username, script_name, script_data FROM sieve_filters
- WHERE filter_type = 'prefilter';",
- "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS
- SELECT md5(script_data), username, script_name, script_data FROM sieve_filters
- WHERE filter_type = 'postfilter';"
+ "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS
+ SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias
+ WHERE address!=goto
+ AND active = '1'
+ AND sogo_visible = '1'
+ AND address NOT LIKE '@%'
+ GROUP BY goto;",
+ // START
+ // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this
+ // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X
+ "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS
+ SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl
+ WHERE send_as NOT LIKE '@%'
+ GROUP BY logged_in_as;",
+ // END
+ "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS
+ SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl
+ WHERE send_as NOT LIKE '@%' AND external = '1'
+ GROUP BY logged_in_as;",
+ "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
+ SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
+ LEFT OUTER JOIN alias_domain ON target_domain=domain
+ GROUP BY username;",
+ "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
+ SELECT md5(script_data), username, script_name, script_data FROM sieve_filters
+ WHERE filter_type = 'prefilter';",
+ "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS
+ SELECT md5(script_data), username, script_name, script_data FROM sieve_filters
+ WHERE filter_type = 'postfilter';"
);
$tables = array(
@@ -251,6 +251,26 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
+ "tags_domain" => array(
+ "cols" => array(
+ "tag_name" => "VARCHAR(255) NOT NULL",
+ "domain" => "VARCHAR(255) NOT NULL"
+ ),
+ "keys" => array(
+ "fkey" => array(
+ "fk_tags_domain" => array(
+ "col" => "domain",
+ "ref" => "domain.domain",
+ "delete" => "CASCADE",
+ "update" => "NO ACTION"
+ )
+ ),
+ "unique" => array(
+ "tag_name" => array("tag_name", "domain")
+ )
+ ),
+ "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+ ),
"tls_policy_override" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
@@ -325,6 +345,26 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
+ "tags_mailbox" => array(
+ "cols" => array(
+ "tag_name" => "VARCHAR(255) NOT NULL",
+ "username" => "VARCHAR(255) NOT NULL"
+ ),
+ "keys" => array(
+ "fkey" => array(
+ "fk_tags_mailbox" => array(
+ "col" => "username",
+ "ref" => "mailbox.username",
+ "delete" => "CASCADE",
+ "update" => "NO ACTION"
+ )
+ ),
+ "unique" => array(
+ "tag_name" => array("tag_name", "username")
+ )
+ ),
+ "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+ ),
"sieve_filters" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php
index 70e1dba5..62e5f909 100644
--- a/data/web/inc/vars.inc.php
+++ b/data/web/inc/vars.inc.php
@@ -148,6 +148,9 @@ $ACCESS_TOKEN_LIFETIME = 86400;
// Logout from mailcow after first OAuth2 session profile request
$OAUTH2_FORGET_SESSION_AFTER_LOGIN = false;
+// Set a limit for mailbox and domain tagging
+$TAGGING_LIMIT = 25;
+
// MAILBOX_DEFAULT_ATTRIBUTES define default attributes for new mailboxes
// These settings will not change existing mailboxes
diff --git a/data/web/js/build/012-api.js b/data/web/js/build/012-api.js
index d979a9b1..68a61b2f 100644
--- a/data/web/js/build/012-api.js
+++ b/data/web/js/build/012-api.js
@@ -156,6 +156,12 @@ $(document).ready(function() {
});
if (!invalid) {
var attr_to_merge = $(this).closest("form").serializeObject();
+ // parse possible JSON Strings
+ for (var [key, value] of Object.entries(attr_to_merge)) {
+ try {
+ attr_to_merge[key] = JSON.parse(attr_to_merge[key]);
+ } catch {}
+ }
var api_attr = $.extend(api_attr, attr_to_merge)
} else {
return false;
@@ -263,6 +269,12 @@ $(document).ready(function() {
});
if (!invalid) {
var attr_to_merge = $(this).closest("form").serializeObject();
+ // parse possible JSON Strings
+ for (var [key, value] of Object.entries(attr_to_merge)) {
+ try {
+ attr_to_merge[key] = JSON.parse(attr_to_merge[key]);
+ } catch {}
+ }
var api_attr = $.extend(api_attr, attr_to_merge)
} else {
return false;
@@ -329,6 +341,7 @@ $(document).ready(function() {
multi_data[id].splice($.inArray($(this).data('item'), multi_data[id]), 1);
multi_data[id].push($(this).data('item'));
}
+
if (typeof $(this).data('text') !== 'undefined') {
$("#DeleteText").empty();
$("#DeleteText").text($(this).data('text'));
@@ -340,9 +353,9 @@ $(document).ready(function() {
$("#ItemsToDelete").empty();
for (var i in data_array) {
data_array[i] = decodeURIComponent(data_array[i]);
- $("#ItemsToDelete").append("<li>" + data_array[i] + "</li>");
+ $("#ItemsToDelete").append("<li>" + escapeHtml(data_array[i]) + "</li>");
}
- })
+ });
$('#ConfirmDeleteModal').modal({
backdrop: 'static',
keyboard: false
diff --git a/data/web/js/build/014-mailcow.js b/data/web/js/build/014-mailcow.js
index 7468310f..bf7e78b1 100644
--- a/data/web/js/build/014-mailcow.js
+++ b/data/web/js/build/014-mailcow.js
@@ -48,7 +48,7 @@ $(document).ready(function() {
$(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
}
$(div).animate({ left: 0},interval);
- }
+ }
// form cache
$('[data-cached-form="true"]').formcache({key: $(this).data('id')});
@@ -273,4 +273,50 @@ $(document).ready(function() {
}
}
});
+
+ // tag boxes
+ $('.tag-box .tag-add').click(function(){
+ addTag(this);
+ });
+ $(".tag-box .tag-input").keydown(function (e) {
+ if (e.which == 13){
+ e.preventDefault();
+ addTag(this);
+ }
+ });
+ function addTag(tagAddElem){
+ var tagboxElem = $(tagAddElem).parent();
+ var tagInputElem = $(tagboxElem).find(".tag-input")[0];
+ var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
+
+ var tag = escapeHtml($(tagInputElem).val());
+ var value_tags = [];
+ try {
+ value_tags = JSON.parse($(tagValuesElem).val());
+ } catch {}
+ if (!Array.isArray(value_tags)) value_tags = [];
+ if (value_tags.includes(tag)) return;
+
+ $('<span class="badge badge-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + tag + '</span>').insertBefore('.tag-input').click(function(){
+ var del_tag = unescapeHtml($(this).text());
+ var del_tags = [];
+ try {
+ del_tags = JSON.parse($(tagValuesElem).val());
+ } catch {}
+ if (Array.isArray(del_tags)){
+ del_tags.splice(del_tags.indexOf(del_tag), 1);
+ $(tagValuesElem).val(JSON.stringify(del_tags));
+ }
+ $(this).remove();
+ });
+
+ value_tags.push($(tagInputElem).val());
+ $(tagValuesElem).val(JSON.stringify(value_tags));
+ $(tagInputElem).val('');
+ }
});
+
+
+// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+function escapeHtml(n){var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+function unescapeHtml(t){var n={"&":"&","<":"<",">":">",""":'"',"'":"'","/":"/","`":"`","=":"="};return String(t).replace(/&|<|>|"|'|/|`|=/g,function(t){return n[t]})}
diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js
index 745c4f21..0ede66d8 100644
--- a/data/web/js/site/mailbox.js
+++ b/data/web/js/site/mailbox.js
@@ -236,9 +236,6 @@ $(document).ready(function() {
});
jQuery(function($){
- // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
- var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};
- function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
function unix_time_format(i){return""==i?'<i class="bi bi-x-lg"></i>':new Date(i?1e3*i:0).toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})}
@@ -293,6 +290,7 @@ jQuery(function($){
{"name":"rl","title":"RL","breakpoints":"xs sm md lg","style":{"min-width":"100px","width":"100px"}},
{"name":"backupmx","filterable": false,"style":{"min-width":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md lg","formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}},
{"name":"domain_admins","title":lang.domain_admins,"style":{"word-break":"break-all","min-width":"200px"},"breakpoints":"xs sm md lg","filterable":(role == "admin"),"visible":(role == "admin")},
+ {"name":"tags","title":"Tags","style":{},"breakpoints":"xs sm md lg"},
{"name":"active","filterable": false,"style":{"min-width":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}},
{"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
],
@@ -330,6 +328,13 @@ jQuery(function($){
'<a href="#dnsInfoModal" class="btn btn-xs btn-xs-half btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.domain_name) + '"><i class="bi bi-globe2"></i> DNS</a></div>';
}
+ if (Array.isArray(item.tags)){
+ var tags = '';
+ for (var i = 0; i < item.tags.length; i++)
+ tags += '<span class="badge badge-primary tag-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(item.tags[i]) + '</span>';
+ item.tags = tags;
+ }
+
if (item.backupmx == 1) {
if (item.relay_unknown_only == 1) {
item.domain_name = '<div class="label label-info">Relay Non-Local</div> ' + item.domain_name;
@@ -418,6 +423,7 @@ jQuery(function($){
},
{"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"},
/* {"name":"rl","title":"RL","breakpoints":"all","style":{"width":"125px"}}, */
+ {"name":"tags","title":"Tags","style":{},"breakpoints":"xs sm md lg"},
{"name":"active","filterable": false,"style":{"min-width":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':(0==value?'<i class="bi bi-x-lg"></i>':2==value&&'—');}},
{"name":"action","filterable": false,"sortable": false,"style":{"min-width":"290px","text-align":"right"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
],
@@ -497,6 +503,13 @@ jQuery(function($){
'<div class="progress-bar-mailbox progress-bar progress-bar-' + item.percent_class + '" role="progressbar" aria-valuenow="' + item.percent_in_use + '" aria-valuemin="0" aria-valuemax="100" ' +
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>';
item.username = escapeHtml(item.username);
+
+ if (Array.isArray(item.tags)){
+ var tags = '';
+ for (var i = 0; i < item.tags.length; i++)
+ tags += '<span class="badge badge-primary tag-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(item.tags[i]) + '</span>';
+ item.tags = tags;
+ }
});
}
}),
diff --git a/data/web/json_api.php b/data/web/json_api.php
index c82aba6c..8f0e1398 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -14,17 +14,20 @@ function api_log($_data) {
if ($data == 'csrf_token') {
continue;
}
- if ($value = json_decode($value, true)) {
- unset($value["csrf_token"]);
+
+ $value = json_decode($value, true);
+ if ($value) {
+ if (is_array($value)) unset($value["csrf_token"]);
foreach ($value as $key => &$val) {
if(preg_match("/pass/i", $key)) {
$val = '*';
}
}
- $value = json_encode($value);
+ $value = json_encode($value);
}
$data_var[] = $data . "='" . $value . "'";
}
+
try {
$log_line = array(
'time' => time(),
@@ -41,7 +44,7 @@ function api_log($_data) {
'msg' => 'Redis: '.$e
);
return false;
- }
+ }
}
if (isset($_GET['query'])) {
@@ -82,10 +85,10 @@ if (isset($_GET['query'])) {
if ($action == 'delete') {
$_POST['items'] = $request;
}
-
}
api_log($_POST);
+
$request_incomplete = json_encode(array(
'type' => 'error',
'msg' => 'Cannot find attributes in post data'
@@ -486,7 +489,12 @@ if (isset($_GET['query'])) {
case "domain":
switch ($object) {
case "all":
- $domains = mailbox('get', 'domains');
+ $tags = null;
+ if (isset($_GET['tags']) && $_GET['tags'] != '')
+ $tags = explode(',', $_GET['tags']);
+
+ $domains = mailbox('get', 'domains', null, $tags);
+
if (!empty($domains)) {
foreach ($domains as $domain) {
if ($details = mailbox('get', 'domain_details', $domain)) {
@@ -952,23 +960,20 @@ if (isset($_GET['query'])) {
switch ($object) {
case "all":
case "reduced":
- if (empty($extra)) {
- $domains = mailbox('get', 'domains');
- }
- else {
- $domains = explode(',', $extra);
- }
+ $tags = null;
+ if (isset($_GET['tags']) && $_GET['tags'] != '')
+ $tags = explode(',', $_GET['tags']);
+
+ if (empty($extra)) $domains = mailbox('get', 'domains');
+ else $domains = explode(',', $extra);
+
if (!empty($domains)) {
foreach ($domains as $domain) {
- $mailboxes = mailbox('get', 'mailboxes', $domain);
+ $mailboxes = mailbox('get', 'mailboxes', $domain, $tags);
if (!empty($mailboxes)) {
foreach ($mailboxes as $mailbox) {
- if ($details = mailbox('get', 'mailbox_details', $mailbox, $object)) {
- $data[] = $details;
- }
- else {
- continue;
- }
+ if ($details = mailbox('get', 'mailbox_details', $mailbox, $object)) $data[] = $details;
+ else continue;
}
}
}
@@ -980,7 +985,17 @@ if (isset($_GET['query'])) {
break;
default:
- $data = mailbox('get', 'mailbox_details', $object);
+ $tags = null;
+ if (isset($_GET['tags']) && $_GET['tags'] != '')
+ $tags = explode(',', $_GET['tags']);
+
+ $mailboxes = mailbox('get', 'mailboxes', $object, $tags);
+ if (!empty($mailboxes)) {
+ foreach ($mailboxes as $mailbox) {
+ if ($details = mailbox('get', 'mailbox_details', $mailbox)) $data[] = $details;
+ else continue;
+ }
+ }
process_get_return($data);
break;
}
@@ -1580,13 +1595,25 @@ if (isset($_GET['query'])) {
process_delete_return(dkim('delete', array('domains' => $items)));
break;
case "domain":
- process_delete_return(mailbox('delete', 'domain', array('domain' => $items)));
+ switch ($object){
+ case "tag":
+ process_delete_return(mailbox('delete', 'tags_domain', array('tags' => $items, 'domain' => $extra)));
+ break;
+ default:
+ process_delete_return(mailbox('delete', 'domain', array('domain' => $items)));
+ }
break;
case "alias-domain":
process_delete_return(mailbox('delete', 'alias_domain', array('alias_domain' => $items)));
break;
case "mailbox":
- process_delete_return(mailbox('delete', 'mailbox', array('username' => $items)));
+ switch ($object){
+ case "tag":
+ process_delete_return(mailbox('delete', 'tags_mailbox', array('tags' => $items, 'username' => $extra)));
+ break;
+ default:
+ process_delete_return(mailbox('delete', 'mailbox', array('username' => $items)));
+ }
break;
case "resource":
process_delete_return(mailbox('delete', 'resource', array('name' => $items)));
diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json
index fbbc3d09..8445641f 100644
--- a/data/web/lang/lang.en.json
+++ b/data/web/lang/lang.en.json
@@ -99,6 +99,7 @@
"subscribeall": "Subscribe all folders",
"syncjob": "Add sync job",
"syncjob_hint": "Be aware that passwords need to be saved plain-text!",
+ "tags": "Tags",
"target_address": "Goto addresses",
"target_address_info": "<small>Full email address/es (comma-separated).</small>",
"target_domain": "Target domain",
diff --git a/data/web/templates/edit/domain.twig b/data/web/templates/edit/domain.twig
index 2ece7ff3..74703bfd 100644
--- a/data/web/templates/edit/domain.twig
+++ b/data/web/templates/edit/domain.twig
@@ -23,6 +23,22 @@
<input type="text" class="form-control" name="description" value="{{ result.description }}">
</div>
</div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
+ <div class="col-sm-10">
+ <div class="form-control tag-box">
+ {% for tag in domain_details.tags %}
+ <span data-action='delete_selected' data-item="{{ tag|url_encode }}" data-id="domain_tag_{{ tag }}" data-api-url='delete/domain/tag/{{ domain }}' class="badge badge-primary tag-badge btn-badge">
+ <i class="bi bi-tag-fill"></i>
+ {{ tag }}
+ </span>
+ {% endfor %}
+ <input type="text" class="tag-input">
+ <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+ <input type="hidden" value="" name="tags" class="tag-values" />
+ </div>
+ </div>
+ </div>
<div class="form-group">
<label class="control-label col-sm-2" for="relayhost">{{ lang.edit.relayhost }}</label>
<div class="col-sm-10">
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig
index 5c65541b..e1c3e883 100644
--- a/data/web/templates/edit/mailbox.twig
+++ b/data/web/templates/edit/mailbox.twig
@@ -22,6 +22,22 @@
<input type="text" class="form-control" name="name" value="{{ result.name }}">
</div>
</div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
+ <div class="col-sm-10">
+ <div class="form-control tag-box">
+ {% for tag in mailbox_details.tags %}
+ <span data-action='delete_selected' data-item="{{ tag }}" data-id="mailbox_tag_{{ tag }}" data-api-url='delete/mailbox/tag/{{ mailbox }}' class="badge badge-primary tag-badge btn-badge">
+ <i class="bi bi-tag-fill"></i>
+ {{ tag }}
+ </span>
+ {% endfor %}
+ <input type="text" class="tag-input">
+ <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+ <input type="hidden" value="" name="tags" class="tag-values" />
+ </div>
+ </div>
+ </div>
<div class="form-group">
<label class="control-label col-sm-2" for="quota">{{ lang.edit.quota_mb }}
<br><span id="quotaBadge" class="badge">max. {{ (result.max_new_quota / 1048576) }} MiB</span>
diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig
index 279e4c96..4097145f 100644
--- a/data/web/templates/modals/mailbox.twig
+++ b/data/web/templates/modals/mailbox.twig
@@ -30,6 +30,16 @@
<input type="text" class="form-control" name="name">
</div>
</div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
+ <div class="col-sm-10">
+ <div class="form-control tag-box">
+ <input type="text" class="tag-input">
+ <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+ <input type="hidden" value="" name="tags" class="tag-values" />
+ </div>
+ </div>
+ </div>
<div class="form-group">
<label class="control-label col-sm-2" for="addInputQuota">{{ lang.add.quota_mb }}
<br /><span id="quotaBadge" class="badge">max. - MiB</span>
@@ -94,6 +104,16 @@
<input type="text" class="form-control" name="description">
</div>
</div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
+ <div class="col-sm-10">
+ <div class="form-control tag-box">
+ <input type="text" class="tag-input">
+ <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span>
+ <input type="hidden" value="" name="tags" class="tag-values" />
+ </div>
+ </div>
+ </div>
<div class="form-group">
<label class="control-label col-sm-2" for="aliases">{{ lang.add.max_aliases }}</label>
<div class="col-sm-10">
@@ -188,11 +208,11 @@
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10 btn-group">
{% if not skip_sogo %}
- <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{}' href="#">{{ lang.add.add_domain_only }}</button>
- <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"restart_sogo":"1"}' href="#">{{ lang.add.add_domain_restart }}</button>
+ <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"tags": []}' href="#">{{ lang.add.add_domain_only }}</button>
+ <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"restart_sogo":"1", "tags": []}' href="#">{{ lang.add.add_domain_restart }}</button>
<div class="clearfix visible-xs"></div>
{% else %}
- <button class="btn btn-xs-lg visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{}' href="#">{{ lang.add.add }}</button>
+ <button class="btn btn-xs-lg visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"tags": []}' href="#">{{ lang.add.add }}</button>
{% endif %}
</div>
</div>