From f295b8cd91f9d691ad93c53191e6319282dcae55 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 8 May 2023 12:55:38 +0200 Subject: [PATCH 1/5] [Rspamd] add domain wide footer --- data/conf/rspamd/lua/rspamd.local.lua | 120 +++++++++++++++++++++++++ data/web/edit.php | 2 + data/web/inc/functions.mailbox.inc.php | 73 +++++++++++++++ data/web/json_api.php | 27 +++--- data/web/lang/lang.de-de.json | 5 ++ data/web/lang/lang.en-gb.json | 5 ++ data/web/templates/edit/domain.twig | 28 ++++++ 7 files changed, 248 insertions(+), 12 deletions(-) diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 6318bd23a..3d4716009 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -499,3 +499,123 @@ rspamd_config:register_symbol({ end end }) + +rspamd_config:register_symbol({ + name = 'MOO_FOOTER', + type = 'prefilter', + callback = function(task) + local lua_mime = require "lua_mime" + 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 envfrom = task:get_from(1) + local uname = task:get_user() + if not envfrom or not uname then + return false + end + local uname = uname:lower() + local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case + + local function newline(task) + local t = task:get_newlines_type() + + if t == 'cr' then + return '\r' + elseif t == 'lf' then + return '\n' + end + + return '\r\n' + end + local function redis_cb_footer(err, data) + 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 + 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 or footer.plain) then + rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain) + + -- add footer + local out = {} + local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {} + + local seen_cte + local newline_s = newline(task) + + local function rewrite_ct_cb(name, hdr) + if rewrite.need_rewrite_ct then + if name:lower() == 'content-type' then + local nct = string.format('%s: %s/%s; charset=utf-8', + 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype) + out[#out + 1] = nct + return + elseif name:lower() == 'content-transfer-encoding' then + out[#out + 1] = string.format('%s: %s', + 'Content-Transfer-Encoding', 'quoted-printable') + seen_cte = true + return + end + end + out[#out + 1] = hdr.raw:gsub('\r?\n?$', '') + end + + task:headers_foreach(rewrite_ct_cb, {full = true}) + + if not seen_cte and rewrite.need_rewrite_ct then + out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable') + end + + -- End of headers + out[#out + 1] = newline_s + + if rewrite.out then + for _,o in ipairs(rewrite.out) do + out[#out + 1] = o + end + else + out[#out + 1] = task:get_rawbody() + end + local out_parts = {} + for _,o in ipairs(out) do + if type(o) ~= 'table' then + out_parts[#out_parts + 1] = o + out_parts[#out_parts + 1] = newline_s + else + out_parts[#out_parts + 1] = o[1] + if o[2] then + out_parts[#out_parts + 1] = newline_s + end + end + end + task:set_message(out_parts) + else + rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data) + end + end + 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 + + return true + end, + priority = 1 +}) diff --git a/data/web/edit.php b/data/web/edit.php index 09db796d3..7655b3c3a 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -47,6 +47,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { $quota_notification_bcc = quota_notification_bcc('get', $domain); $rl = ratelimit('get', 'domain', $domain); $rlyhosts = relayhost('get'); + $domain_footer = mailbox('get', 'domain_wide_footer', $domain); $template = 'edit/domain.twig'; $template_data = [ 'acl' => $_SESSION['acl'], @@ -56,6 +57,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { 'rlyhosts' => $rlyhosts, 'dkim' => dkim('details', $domain), 'domain_details' => $result, + 'domain_footer' => $domain_footer, ]; } } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 4e036b99c..dace3e8ac 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3320,6 +3320,45 @@ 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; + } + 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['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) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('redis_error', $e) + ); + return false; + } + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('domain_footer_modified', htmlspecialchars($domain)) + ); + break; } break; case 'get': @@ -4399,6 +4438,40 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } return $resourcedata; break; + case 'domain_wide_footer': + $domain = idn_to_ascii(strtolower(trim($_data)), 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'], $_data)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + + try { + $footers = $redis->hGet('DOMAIN_WIDE_FOOTER', $domain); + $footers = json_decode($footers, true); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('redis_error', $e) + ); + return false; + } + + return $footers; + break; } break; case 'delete': diff --git a/data/web/json_api.php b/data/web/json_api.php index ec028fe4b..e21219c59 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -288,18 +288,18 @@ if (isset($_GET['query'])) { case "domain-admin": process_add_return(domain_admin('add', $attr)); break; - case "sso": - switch ($object) { - case "domain-admin": - $data = domain_admin_sso('issue', $attr); - if($data) { - echo json_encode($data); - exit(0); - } - process_add_return($data); - break; - } - break; + case "sso": + switch ($object) { + case "domain-admin": + $data = domain_admin_sso('issue', $attr); + if($data) { + echo json_encode($data); + exit(0); + } + process_add_return($data); + break; + } + break; case "admin": process_add_return(admin('add', $attr)); break; @@ -1867,6 +1867,9 @@ 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))); break; diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 6b280bbb8..f1378500d 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -576,6 +576,10 @@ "disable_login": "Login verbieten (Mails werden weiterhin angenommen)", "domain": "Domain bearbeiten", "domain_admin": "Domain-Administrator bearbeiten", + "domain_footer": "Domain wide footer", + "domain_footer_html": "HTML footer", + "domain_footer_info": "Domain wide footer werden allen E-Mails hinzugefügt, die von der angegebenen Domain gesendet werden.", + "domain_footer_plain": "PLAIN footer", "domain_quota": "Domain Speicherplatz gesamt (MiB)", "domains": "Domains", "dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen", @@ -1011,6 +1015,7 @@ "domain_admin_added": "Domain-Administrator %s wurde angelegt", "domain_admin_modified": "Änderungen an Domain-Administrator %s wurden gespeichert", "domain_admin_removed": "Domain-Administrator %s wurde entfernt", + "domain_footer_modified": "Änderungen an Domain Footer %s wurden gespeichert", "domain_modified": "Änderungen an Domain %s wurden gespeichert", "domain_removed": "Domain %s wurde entfernt", "dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index e53fe896c..30be210cc 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -576,6 +576,10 @@ "disable_login": "Disallow login (incoming mail is still accepted)", "domain": "Edit domain", "domain_admin": "Edit domain administrator", + "domain_footer": "Domain wide footer", + "domain_footer_html": "HTML footer", + "domain_footer_info": "Domain wide footers will be added to all emails sent from the specified domain.", + "domain_footer_plain": "PLAIN footer", "domain_quota": "Domain quota", "domains": "Domains", "dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)", @@ -1018,6 +1022,7 @@ "domain_admin_added": "Domain administrator %s has been added", "domain_admin_modified": "Changes to domain administrator %s have been saved", "domain_admin_removed": "Domain administrator %s has been removed", + "domain_footer_modified": "Changes to domain footer %s have been saved", "domain_modified": "Changes to domain %s have been saved", "domain_removed": "Domain %s has been removed", "dovecot_restart_success": "Dovecot was restarted successfully", diff --git a/data/web/templates/edit/domain.twig b/data/web/templates/edit/domain.twig index 0c424887d..16c1a9661 100644 --- a/data/web/templates/edit/domain.twig +++ b/data/web/templates/edit/domain.twig @@ -7,6 +7,7 @@ +
@@ -229,6 +230,33 @@
+
+
+
+

{{ lang.edit.domain_footer }}

+

{{ lang.edit.domain_footer_info|raw }}

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
{% else %} {{ parent() }} From 5ae9605e7747d2588609f9b34245264c03a811ac Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 12 Sep 2023 12:19:46 +0200 Subject: [PATCH 2/5] [Rspamd] domain-wide-footer add jinja templating --- data/conf/rspamd/lua/rspamd.local.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 3d4716009..eb1659b36 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -505,6 +505,7 @@ rspamd_config:register_symbol({ type = 'prefilter', callback = function(task) 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" @@ -542,6 +543,15 @@ rspamd_config:register_symbol({ if footer and type(footer) == "table" and (footer.html or footer.plain) then rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain) + local replacements = { + email = uname + } + if footer.html then + footer.html = lua_util.jinja_template(footer.html, replacements, true) + end + if footer.plain then + footer.plain = lua_util.jinja_template(footer.plain, replacements, true) + end -- add footer local out = {} From 2111115a7329300e0c0433fe6c238bb1f7a48c3e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 13 Sep 2023 12:42:12 +0200 Subject: [PATCH 3/5] [Rspamd] domain-wide-footer add more template vars --- data/conf/rspamd/lua/rspamd.local.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index eb1659b36..95b3d4092 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -543,8 +543,21 @@ rspamd_config:register_symbol({ if footer and type(footer) == "table" and (footer.html or footer.plain) then rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain) + + local envfrom_mime = task:get_from(2) + local from_name = "" + if envfrom_mime and envfrom_mime[1].name then + from_name = envfrom_mime[1].name + elseif envfrom and envfrom[1].name then + from_name = envfrom[1].name + end + local replacements = { - email = uname + auth_user = uname, + from_user = envfrom[1].user, + from_name = from_name, + from_addr = envfrom[1].addr, + from_domain = envfrom[1].domain:lower() } if footer.html then footer.html = lua_util.jinja_template(footer.html, replacements, true) From 8d792fbd624ce7d23ae8bcf88b49e28d654c3e2e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 13 Sep 2023 13:03:46 +0200 Subject: [PATCH 4/5] [Rspamd] domain-wide-footer update description --- data/web/lang/lang.de-de.json | 2 +- data/web/lang/lang.en-gb.json | 2 +- data/web/templates/edit/domain.twig | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index d0c494da5..d073c45c5 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -583,7 +583,7 @@ "domain_admin": "Domain-Administrator bearbeiten", "domain_footer": "Domain wide footer", "domain_footer_html": "HTML footer", - "domain_footer_info": "Domain wide footer werden allen E-Mails hinzugefügt, die von der angegebenen Domain gesendet werden.", + "domain_footer_info": "Domain wide footer werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.
Die folgenden Variablen können für den Footer benutzt werden:", "domain_footer_plain": "PLAIN footer", "domain_quota": "Domain Speicherplatz gesamt (MiB)", "domains": "Domains", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index c8dded338..3896a79a4 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -585,7 +585,7 @@ "domain_admin": "Edit domain administrator", "domain_footer": "Domain wide footer", "domain_footer_html": "HTML footer", - "domain_footer_info": "Domain wide footers will be added to all emails sent from the specified domain.", + "domain_footer_info": "Domain-wide footers are added to all outgoing emails associated with an address within this domain.
The following variables can be used for the footer:", "domain_footer_plain": "PLAIN footer", "domain_quota": "Domain quota", "domains": "Domains", diff --git a/data/web/templates/edit/domain.twig b/data/web/templates/edit/domain.twig index 997788df7..527a312c8 100644 --- a/data/web/templates/edit/domain.twig +++ b/data/web/templates/edit/domain.twig @@ -281,7 +281,12 @@

{{ lang.edit.domain_footer }}

{{ lang.edit.domain_footer_info|raw }}

-
+
{= auth_user =}   - Authenticated Username specified by an MTA
+{= from_user =}   - Parsed user part of email headers, e.g for "moo@mailcow.tld" it returns "moo"
+{= from_name =}   - Parsed name of email headers, e.g for "Mailcow <moo@mailcow.tld>" it returns "Mailcow"
+{= from_addr =}   - Parsed address part of email headers
+{= from_domain =} - Parsed domain part of email headers
+
From acee74282217b07d858dba92f73a2cb2bffdaffc Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 13 Sep 2023 15:08:07 +0200 Subject: [PATCH 5/5] [Web] move domain-wide-footer vars info to lang files --- data/web/lang/lang.en-gb.json | 7 +++++++ data/web/templates/edit/domain.twig | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 3896a79a4..22475bb28 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -586,6 +586,13 @@ "domain_footer": "Domain wide footer", "domain_footer_html": "HTML footer", "domain_footer_info": "Domain-wide footers are added to all outgoing emails associated with an address within this domain.
The following variables can be used for the footer:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Authenticated Username specified by an MTA", + "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" + }, "domain_footer_plain": "PLAIN footer", "domain_quota": "Domain quota", "domains": "Domains", diff --git a/data/web/templates/edit/domain.twig b/data/web/templates/edit/domain.twig index 527a312c8..5e9eb54be 100644 --- a/data/web/templates/edit/domain.twig +++ b/data/web/templates/edit/domain.twig @@ -281,11 +281,11 @@

{{ lang.edit.domain_footer }}

{{ lang.edit.domain_footer_info|raw }}

-
{= auth_user =}   - Authenticated Username specified by an MTA
-{= from_user =}   - Parsed user part of email headers, e.g for "moo@mailcow.tld" it returns "moo"
-{= from_name =}   - Parsed name of email headers, e.g for "Mailcow <moo@mailcow.tld>" it returns "Mailcow"
-{= from_addr =}   - Parsed address part of email headers
-{= from_domain =} - Parsed domain part of email headers
+
{{ lang.edit.domain_footer_info_vars.auth_user }}
+{{ lang.edit.domain_footer_info_vars.from_user }}
+{{ lang.edit.domain_footer_info_vars.from_name }}
+{{ lang.edit.domain_footer_info_vars.from_addr }}
+{{ lang.edit.domain_footer_info_vars.from_domain }}