diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index e6d329b5..8af7619c 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -79,6 +79,9 @@ EOF redis-cli -h redis-mailcow SLAVEOF NO ONE fi +# Provide additional lua modules +ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so + chown -R _rspamd:_rspamd /var/lib/rspamd \ /etc/rspamd/local.d \ /etc/rspamd/override.d \ diff --git a/data/conf/rspamd/dynmaps/footer.php b/data/conf/rspamd/dynmaps/footer.php new file mode 100644 index 00000000..ca18697a --- /dev/null +++ b/data/conf/rspamd/dynmaps/footer.php @@ -0,0 +1,89 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("FOOTER: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get Domain +$domain = $headers['Domain']; +// Get Username +$username = $headers['Username']; +// define empty footer +$empty_footer = json_encode(array( + 'html' => '', + 'plain' => '', + 'vars' => array() +)); + +error_log("FOOTER: checking for domain " . $domain . " and user " . $username . PHP_EOL); + +try { + $stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude` FROM `domain_wide_footer` + WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $domain + )); + $footer = $stmt->fetch(PDO::FETCH_ASSOC); + if (in_array($username, json_decode($footer['mbox_exclude']))){ + $footer = false; + } + if (empty($footer)){ + echo $empty_footer; + exit; + } + error_log("FOOTER: " . json_encode($footer) . PHP_EOL); + + $stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username + )); + $custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes']; + if (empty($custom_attributes)){ + $custom_attributes = (object)array(); + } +} +catch (Exception $e) { + error_log("FOOTER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + + +// return footer +$footer["vars"] = $custom_attributes; +echo json_encode($footer); diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 459b2339..44be2e34 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -527,12 +527,11 @@ rspamd_config:register_symbol({ name = 'MOO_FOOTER', type = 'prefilter', callback = function(task) + local cjson = require "cjson" local lua_mime = require "lua_mime" local lua_util = require "lua_util" local rspamd_logger = require "rspamd_logger" - local rspamd_redis = require "rspamd_redis" - local ucl = require "ucl" - local redis_params = rspamd_parse_redis_server('footer') + local rspamd_http = require "rspamd_http" local envfrom = task:get_from(1) local uname = task:get_user() if not envfrom or not uname then @@ -541,6 +540,7 @@ rspamd_config:register_symbol({ local uname = uname:lower() local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case + -- determine newline type local function newline(task) local t = task:get_newlines_type() @@ -552,20 +552,19 @@ rspamd_config:register_symbol({ return '\r\n' end - local function redis_cb_footer(err, data) + -- retrieve footer + local function footer_cb(err_message, code, data, headers) if err or type(data) ~= 'string' then rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err) else + -- parse json string - local parser = ucl.parser() - local res,err = parser:parse_string(data) - if not res then + local footer = cjson.decode(data) + if not footer then rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err) else - local footer = parser:get_object() - if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then - rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain) + rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars) local envfrom_mime = task:get_from(2) local from_name = "" @@ -575,6 +574,7 @@ rspamd_config:register_symbol({ from_name = envfrom[1].name end + -- default replacements local replacements = { auth_user = uname, from_user = envfrom[1].user, @@ -582,10 +582,20 @@ rspamd_config:register_symbol({ from_addr = envfrom[1].addr, from_domain = envfrom[1].domain:lower() } - if footer.html then + -- add custom mailbox attributes + if footer.vars and type(footer.vars) == "string" then + local footer_vars = cjson.decode(footer.vars) + + if type(footer_vars) == "table" then + for key, value in pairs(footer_vars) do + replacements[key] = value + end + end + end + if footer.html and footer.html ~= "" then footer.html = lua_util.jinja_template(footer.html, replacements, true) end - if footer.plain then + if footer.plain and footer.plain ~= "" then footer.plain = lua_util.jinja_template(footer.plain, replacements, true) end @@ -653,17 +663,14 @@ rspamd_config:register_symbol({ end end - local redis_ret_footer = rspamd_redis_make_request(task, - redis_params, -- connect params - env_from_domain, -- hash key - false, -- is write - redis_cb_footer, --callback - 'HGET', -- command - {"DOMAIN_WIDE_FOOTER", env_from_domain} -- arguments - ) - if not redis_ret_footer then - rspamd_logger.infox(rspamd_config, "cannot make request to load footer for domain") - end + -- fetch footer + rspamd_http.request({ + task=task, + url='http://nginx:8081/footer.php', + body='', + callback=footer_cb, + headers={Domain=env_from_domain,Username=uname}, + }) return true end, diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 1d262168..a363dcbb 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -3137,6 +3137,86 @@ paths: type: string type: object summary: Update domain + /api/v1/edit/domain/footer: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - edit + - domain_wide_footer + - domains: + - mailcow.tld + html: "
foo {= foo =}" + plain: "- + You can update the footer of one or more domains per request. + operationId: Update domain wide footer + requestBody: + content: + application/json: + schema: + example: + attr: + html: "
foo {= foo =}" + plain: "foo {= foo =}" + mbox_exclude: + - moo@mailcow.tld + items: mailcow.tld + properties: + attr: + properties: + html: + description: Footer text in HTML format + type: string + plain: + description: Footer text in PLAIN text format + type: string + mbox_exclude: + description: Array of mailboxes to exclude from domain wide footer + type: object + type: object + items: + description: contains a list of domain names where you want to update the footer + type: array + items: + type: string + type: object + summary: Update domain wide footer /api/v1/edit/fail2ban: post: responses: @@ -3336,6 +3416,86 @@ paths: type: object type: object summary: Update mailbox + /api/v1/edit/mailbox/custom-attribute: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - edit + - mailbox_custom_attribute + - mailboxes: + - moo@mailcow.tld + attribute: + - role + - foo + value: + - cow + - bar + - null + msg: + - mailbox_modified + - moo@mailcow.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: + - Mailboxes + description: >- + You can update custom attributes of one or more mailboxes per request. + operationId: Update mailbox custom attributes + requestBody: + content: + application/json: + schema: + example: + attr: + attribute: + - role + - foo + value: + - cow + - bar + items: + - moo@mailcow.tld + properties: + attr: + properties: + attribute: + description: Array of attribute keys + type: object + value: + description: Array of attribute values + type: object + type: object + items: + description: contains list of mailboxes you want update + type: object + type: object + summary: Update mailbox custom attributes /api/v1/edit/mailq: post: responses: @@ -5581,6 +5741,7 @@ paths: sogo_access: "1" tls_enforce_in: "0" tls_enforce_out: "0" + custom_attributes: {} domain: domain3.tld is_relayed: 0 local_part: info diff --git a/data/web/edit.php b/data/web/edit.php index 8061441b..83ae1467 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -58,6 +58,8 @@ if (isset($_SESSION['mailcow_cc_role'])) { 'dkim' => dkim('details', $domain), 'domain_details' => $result, 'domain_footer' => $domain_footer, + 'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]), + 'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address') ]; } } @@ -218,6 +220,7 @@ $js_minifier->add('/web/js/site/pwgen.js'); $template_data['result'] = $result; $template_data['return_to'] = $_SESSION['return_to']; $template_data['lang_user'] = json_encode($lang['user']); +$template_data['lang_admin'] = json_encode($lang['admin']); $template_data['lang_datatables'] = json_encode($lang['datatables']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 298f607d..7c2955f6 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3264,6 +3264,62 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return true; break; + case 'mailbox_custom_attribute': + $_data['attribute'] = isset($_data['attribute']) ? $_data['attribute'] : array(); + $_data['attribute'] = is_array($_data['attribute']) ? $_data['attribute'] : array($_data['attribute']); + $_data['attribute'] = array_map(function($value) { return str_replace(' ', '', $value); }, $_data['attribute']); + $_data['value'] = isset($_data['value']) ? $_data['value'] : array(); + $_data['value'] = is_array($_data['value']) ? $_data['value'] : array($_data['value']); + $attributes = (object)array_combine($_data['attribute'], $_data['value']); + $mailboxes = is_array($_data['mailboxes']) ? $_data['mailboxes'] : array($_data['mailboxes']); + + foreach ($mailboxes as $mailbox) { + if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('username_invalid', $mailbox) + ); + continue; + } + $is_now = mailbox('get', 'mailbox_details', $mailbox); + if(!empty($is_now)){ + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + + + $stmt = $pdo->prepare("UPDATE `mailbox` + SET `custom_attributes` = :custom_attributes + WHERE username = :username"); + $stmt->execute(array( + ":username" => $mailbox, + ":custom_attributes" => json_encode($attributes) + )); + + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('mailbox_modified', $mailbox) + ); + } + + return true; + break; case 'resource': if (!is_array($_data['name'])) { $names = array(); @@ -3343,44 +3399,79 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; - case 'domain_wide_footer': - $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); - if (!is_valid_domain_name($domain)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'domain_invalid' - ); - return false; + case 'domain_wide_footer': + if (!is_array($_data['domains'])) { + $domains = array(); + $domains[] = $_data['domains']; } - 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' - ); - return false; + else { + $domains = $_data['domains']; } - $footers = array(); - $footers['html'] = isset($_data['footer_html']) ? $_data['footer_html'] : ''; - $footers['plain'] = isset($_data['footer_plain']) ? $_data['footer_plain'] : ''; - try { - $redis->hSet('DOMAIN_WIDE_FOOTER', $domain, json_encode($footers)); - } - catch (RedisException $e) { + foreach ($domains as $domain) { + $domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46); + if (!is_valid_domain_name($domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'domain_invalid' + ); + return false; + } + 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' + ); + return false; + } + + $footers = array(); + $footers['html'] = isset($_data['html']) ? $_data['html'] : ''; + $footers['plain'] = isset($_data['plain']) ? $_data['plain'] : ''; + $footers['mbox_exclude'] = array(); + if (isset($_data["mbox_exclude"])){ + if (!is_array($_data["mbox_exclude"])) { + $_data["mbox_exclude"] = array($_data["mbox_exclude"]); + } + foreach ($_data["mbox_exclude"] as $mailbox) { + if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('username_invalid', $mailbox) + ); + } else { + array_push($footers['mbox_exclude'], $mailbox); + } + } + } + try { + $stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain"); + $stmt->execute(array(':domain' => $domain)); + $stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`) VALUES (:domain, :html, :plain, :mbox_exclude)"); + $stmt->execute(array( + ':domain' => $domain, + ':html' => $footers['html'], + ':plain' => $footers['plain'], + ':mbox_exclude' => json_encode($footers['mbox_exclude']), + )); + } + catch (PDOException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => $e->getMessage() + ); + return false; + } $_SESSION['return'][] = array( - 'type' => 'danger', + 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('redis_error', $e) + 'msg' => array('domain_footer_modified', htmlspecialchars($domain)) ); - return false; } - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('domain_footer_modified', htmlspecialchars($domain)) - ); break; } break; @@ -3934,13 +4025,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { return false; } - $stmt = $pdo->prepare("SELECT `id` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain"); + $stmt = $pdo->prepare("SELECT `id`, `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain"); $stmt->execute(array( ':domain' => $_data, )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { - $aliases[] = $row['id']; + if ($_extra == "address"){ + $aliases[] = $row['address']; + } else { + $aliases[] = $row['id']; + } } return $aliases; break; @@ -4292,6 +4387,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `mailbox`.`modified`, `quota2`.`bytes`, `attributes`, + `custom_attributes`, `quota2`.`messages` FROM `mailbox`, `quota2`, `domain` WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL) @@ -4312,6 +4408,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `mailbox`.`modified`, `quota2replica`.`bytes`, `attributes`, + `custom_attributes`, `quota2replica`.`messages` FROM `mailbox`, `quota2replica`, `domain` WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL) @@ -4334,6 +4431,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['quota'] = $row['quota']; $mailboxdata['messages'] = $row['messages']; $mailboxdata['attributes'] = json_decode($row['attributes'], true); + $mailboxdata['custom_attributes'] = json_decode($row['custom_attributes'], true); $mailboxdata['quota_used'] = intval($row['bytes']); $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100); $mailboxdata['created'] = $row['created']; @@ -4514,19 +4612,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } try { - $footers = $redis->hGet('DOMAIN_WIDE_FOOTER', $domain); - $footers = json_decode($footers, true); + $stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude` FROM `domain_wide_footer` + WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $domain + )); + $footer = $stmt->fetch(PDO::FETCH_ASSOC); } - catch (RedisException $e) { + catch (PDOException $e) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('redis_error', $e) + 'msg' => $e->getMessage() ); return false; } - return $footers; + return $footer; break; } break; diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 2ce6a28c..4ea79d9b 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 = "15112023_1536"; + $db_version = "21112023_1644"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -267,6 +267,20 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "domain_wide_footer" => array( + "cols" => array( + "domain" => "VARCHAR(255) NOT NULL", + "html" => "LONGTEXT", + "plain" => "LONGTEXT", + "mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')", + ), + "keys" => array( + "primary" => array( + "" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "tags_domain" => array( "cols" => array( "tag_name" => "VARCHAR(255) NOT NULL", @@ -344,6 +358,7 @@ function init_db_schema() { "local_part" => "VARCHAR(255) NOT NULL", "domain" => "VARCHAR(255) NOT NULL", "attributes" => "JSON", + "custom_attributes" => "JSON NOT NULL DEFAULT ('{}')", "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", "multiple_bookings" => "INT NOT NULL DEFAULT -1", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", diff --git a/data/web/js/site/edit.js b/data/web/js/site/edit.js index cd938cd8..d6895498 100644 --- a/data/web/js/site/edit.js +++ b/data/web/js/site/edit.js @@ -199,6 +199,23 @@ jQuery(function($){ }); } + function add_table_row(table_id, type) { + var row = $(''); + if (type == "mbox_attr") { + cols = ''; + cols += ''; + cols += '' + lang_admin.remove_row + ''; + } + row.append(cols); + table_id.append(row); + } + $('#mbox_attr_table').on('click', 'tr a', function (e) { + e.preventDefault(); + $(this).parents('tr').remove(); + }); + $('#add_mbox_attr_row').click(function() { + add_table_row($('#mbox_attr_table'), "mbox_attr"); + }); // detect element visibility changes function onVisible(element, callback) { diff --git a/data/web/json_api.php b/data/web/json_api.php index b375bc8e..668be0d1 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1867,8 +1867,6 @@ if (isset($_GET['query'])) { case "quota_notification_bcc": process_edit_return(quota_notification_bcc('edit', $attr)); break; - case "domain-wide-footer": - process_edit_return(mailbox('edit', 'domain_wide_footer', $attr)); break; case "mailq": process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr))); @@ -1881,6 +1879,9 @@ if (isset($_GET['query'])) { case "template": process_edit_return(mailbox('edit', 'mailbox_templates', array_merge(array('ids' => $items), $attr))); break; + case "custom-attribute": + process_edit_return(mailbox('edit', 'mailbox_custom_attribute', array_merge(array('mailboxes' => $items), $attr))); + break; default: process_edit_return(mailbox('edit', 'mailbox', array_merge(array('username' => $items), $attr))); break; @@ -1900,6 +1901,9 @@ if (isset($_GET['query'])) { case "template": process_edit_return(mailbox('edit', 'domain_templates', array_merge(array('ids' => $items), $attr))); break; + case "footer": + process_edit_return(mailbox('edit', 'domain_wide_footer', array_merge(array('domains' => $items), $attr))); + break; default: process_edit_return(mailbox('edit', 'domain', array_merge(array('domain' => $items), $attr))); break; diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 75c66af6..5737a664 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -574,6 +574,7 @@ "client_secret": "Client-Secret", "comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.", "created_on": "Erstellt am", + "custom_attributes": "benutzerdefinierte Attribute", "delete1": "Lösche Nachricht nach Übertragung vom Quell-Server", "delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind", "delete2duplicates": "Lösche Duplikate im Ziel", @@ -614,6 +615,7 @@ "max_quota": "Max. Größe per Mailbox (MiB)", "maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll
(0 = alle Nachrichten kopieren)", "maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)", + "mbox_exclude": "Mailboxen ausschließen", "mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.", "mins_interval": "Intervall (min)", "multiple_bookings": "Mehrfaches Buchen", @@ -1125,6 +1127,7 @@ "apple_connection_profile_complete": "Dieses Verbindungsprofil beinhaltet neben IMAP- und SMTP-Konfigurationen auch Pfade für die Konfiguration von CalDAV (Kalender) und CardDAV (Adressbücher) für ein Apple-Gerät.", "apple_connection_profile_mailonly": "Dieses Verbindungsprofil beinhaltet IMAP- und SMTP-Konfigurationen für ein Apple-Gerät.", "apple_connection_profile_with_app_password": "Es wird ein neues App-Passwort erzeugt und in das Profil eingefügt, damit bei der Einrichtung kein Passwort eingegeben werden muss. Geben Sie das Profil nicht weiter, da es einen vollständigen Zugriff auf Ihr Postfach ermöglicht.", + "attribute": "Attribut", "change_password": "Passwort ändern", "change_password_hint_app_passwords": "Ihre Mailbox hat %d App-Passwörter, die nicht geändert werden. Um diese zu verwalten, gehen Sie bitte zum App-Passwörter-Tab.", "clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen", @@ -1244,6 +1247,7 @@ "tls_policy_warning": "Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.
Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox direkt zugeordnet sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse (\"Spam-Alias-Adressen\"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.", "user_settings": "Benutzereinstellungen", "username": "Benutzername", + "value": "Wert", "verify": "Verifizieren", "waiting": "Warte auf Ausführung", "week": "Woche", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index da97bebd..723ed62e 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -576,6 +576,7 @@ "client_secret": "Client secret", "comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview", "created_on": "Created on", + "custom_attributes": "Custom attributes", "delete1": "Delete from source when completed", "delete2": "Delete messages on destination that are not on source", "delete2duplicates": "Delete duplicates on destination", @@ -592,7 +593,8 @@ "from_user": "{= from_user =} - From user part of envelope, e.g for \"moo@mailcow.tld\" it returns \"moo\"", "from_name": "{= from_name =} - From name of envelope, e.g for \"Mailcow <moo@mailcow.tld>\" it returns \"Mailcow\"", "from_addr": "{= from_addr =} - From address part of envelope", - "from_domain": "{= from_domain =} - From domain part of envelope" + "from_domain": "{= from_domain =} - From domain part of envelope", + "custom": "{= foo =} - If mailbox has the custom attribute \"foo\" with value \"bar\" it returns \"bar\"" }, "domain_footer_plain": "PLAIN footer", "domain_quota": "Domain quota", @@ -623,6 +625,7 @@ "max_quota": "Max. quota per mailbox (MiB)", "maxage": "Maximum age of messages in days that will be polled from remote
(0 = ignore age)", "maxbytespersecond": "Max. bytes per second
(0 = unlimited)", + "mbox_exclude": "Exclude mailboxes", "mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.", "mins_interval": "Interval (min)", "multiple_bookings": "Multiple bookings", @@ -1141,6 +1144,7 @@ "apple_connection_profile_complete": "This connection profile includes IMAP and SMTP parameters as well as CalDAV (calendars) and CardDAV (contacts) paths for an Apple device.", "apple_connection_profile_mailonly": "This connection profile includes IMAP and SMTP configuration parameters for an Apple device.", "apple_connection_profile_with_app_password": "A new app password is generated and added to the profile so that no password needs to be entered when setting up your device. Please do not share the file as it grants full access to your mailbox.", + "attribute": "Attribute", "change_password": "Change password", "change_password_hint_app_passwords": "Your account has %d app passwords that will not be changed. To manage these, go to the App passwords tab.", "clear_recent_successful_connections": "Clear seen successful connections", @@ -1271,6 +1275,7 @@ "tls_policy_warning": "Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.
This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses with only this single mailbox as target.", "user_settings": "User settings", "username": "Username", + "value": "Value", "verify": "Verify", "waiting": "Waiting", "week": "week", diff --git a/data/web/templates/edit.twig b/data/web/templates/edit.twig index af83a31d..375018c6 100644 --- a/data/web/templates/edit.twig +++ b/data/web/templates/edit.twig @@ -24,6 +24,7 @@