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
[Rspamd] add domain wide footer
f295b8cd
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
diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua
index 6318bd23..3d471600 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 09db796d..7655b3c3 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 4e036b99..dace3e8a 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 ec028fe4..e21219c5 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 6b280bbb..f1378500 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 e53fe896..30be210c 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 0c424887..16c1a966 100644
--- a/data/web/templates/edit/domain.twig
+++ b/data/web/templates/edit/domain.twig
@@ -7,6 +7,7 @@
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dratelimit">{{ lang.edit.ratelimit }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dspamfilter">{{ lang.edit.spam_filter }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dqwbcc">{{ lang.edit.quota_warning_bcc }}</button></li>
+ <li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#dfooter">{{ lang.edit.domain_footer }}</button></li>
</ul>
<hr>
<div class="tab-content">
@@ -229,6 +230,33 @@
</div>
</div>
</div>
+ <div id="dfooter" class="tab-pane fade" role="tabpanel" aria-labelledby="domain-footer">
+ <div class="row">
+ <div class="col-sm-12">
+ <h4>{{ lang.edit.domain_footer }}</h4>
+ <p>{{ lang.edit.domain_footer_info|raw }}</p>
+ <form class="form-horizontal" data-id="domain_footer">
+ <div class="row mb-2">
+ <label class="control-label col-sm-2" for="domain_footer_html">{{ lang.edit.domain_footer_html }}:</label>
+ <div class="col-sm-10">
+ <textarea spellcheck="false" autocorrect="off" autocapitalize="none" class="form-control" rows="10" id="domain_footer_html" name="footer_html">{{ domain_footer.html }}</textarea>
+ </div>
+ </div>
+ <div class="row mb-4">
+ <label class="control-label col-sm-2" for="domain_footer_plain">{{ lang.edit.domain_footer_plain }}:</label>
+ <div class="col-sm-10">
+ <textarea spellcheck="false" autocorrect="off" autocapitalize="none" class="form-control" rows="10" id="domain_footer_plain" name="footer_plain">{{ domain_footer.plain }}</textarea>
+ </div>
+ </div>
+ <div class="row">
+ <div class="offset-sm-2 col-sm-10">
+ <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="domain_footer" data-item="domain_footer" data-api-url='edit/domain-wide-footer' data-api-attr='{"domain":"{{ domain }}"}' href="#">{{ lang.edit.save }}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
</div>
{% else %}
{{ parent() }}