diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua
index b29a2446..acc40559 100644
--- a/data/conf/rspamd/lua/rspamd.local.lua
+++ b/data/conf/rspamd/lua/rspamd.local.lua
@@ -522,3 +522,146 @@ 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 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 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)
+
+ 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 = {
+ 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)
+ end
+ if footer.plain then
+ footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
+ end
+
+ -- 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 55bc050d..8061441b 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 8bc6da22..fe41028b 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':
@@ -4432,6 +4471,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 16c78baf..b375bc8e 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -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 e7c6682a..d073c45c 100644
--- a/data/web/lang/lang.de-de.json
+++ b/data/web/lang/lang.de-de.json
@@ -581,6 +581,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 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",
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
@@ -1017,6 +1021,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 f12595f2..22475bb2 100644
--- a/data/web/lang/lang.en-gb.json
+++ b/data/web/lang/lang.en-gb.json
@@ -583,6 +583,17 @@
"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 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",
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
@@ -1026,6 +1037,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 9b3ee6ef..5e9eb54b 100644
--- a/data/web/templates/edit/domain.twig
+++ b/data/web/templates/edit/domain.twig
@@ -8,6 +8,7 @@