git subrepo clone https://github.com/mailcow/mailcow-dockerized.git mailcow/src/mailcow-dockerized
subrepo: subdir: "mailcow/src/mailcow-dockerized"
merged: "a832becb"
upstream: origin: "https://github.com/mailcow/mailcow-dockerized.git"
branch: "master"
commit: "a832becb"
git-subrepo: version: "0.4.3"
origin: "???"
commit: "???"
Change-Id: If5be2d621a211e164c9b6577adaa7884449f16b5
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map
new file mode 100644
index 0000000..fb42628
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_asn.map
@@ -0,0 +1,30 @@
+# High spam networks, disabled by default
+# ASN SCORE DESC
+# Remove comment to enable score
+#201942 5 #Soltia Consulting SL - ipinfo.io
+#16276 2 #OVH
+#12876 2 #ONLINE S.A.S
+#31034 5 #ARUBA-ASN, IT
+#12874 5 #FASTWEB, IT
+#30823 3 #PKV spam
+#42831 5 #UK Dedicated Servers Ltd
+#29119 5 #Aire Networks del Mediterraneo S.L.U.
+#13335 5 #Cloudflare
+#28753 5 #Leaseweb
+#61272 5 #Informacines sistemos ir technologijos
+#53755 5 #Input Output Flood LLC
+#29422 5 #FICIX Helsinki
+#62255 4 #Asmunda New Media Ltd
+#14061 4 #Digitalocean
+#55293 4 #A2 Hosting
+#63018 4 #US Dedicated
+#197518 2 #RACKMARKT
+#44493 2
+#46606 2
+#49505 2
+#21100 2
+#197695 2
+#198068 2
+#43146 2
+#49100 4
+#39364 4
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_languages.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_languages.map
new file mode 100644
index 0000000..cf9ce3e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_languages.map
@@ -0,0 +1 @@
+# Regex! /de/ will also match /de_at/ etc.
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_words.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_words.map
new file mode 100644
index 0000000..0d9af8b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_words.map
@@ -0,0 +1,29 @@
+/\serotic\s/i
+/\serection\s/i
+/\ssexy\s/i
+/\sass\s/i
+/\sviagra\s/i
+/\stits\s/i
+/\stitty\s/i
+/\stitties\s/i
+/\scum\s/i
+/\ssperm\s/i
+/\sslut\s/i
+/\sporn\s/i
+/\scock\s/i
+/\spharma\s/i
+/\spharmacy\s/i
+/\sseo\s/i
+/\sjackpot\s/i
+/\slottery\s/i
+/bitcoin/i
+/trojaner/i
+/malware/i
+/\sscooter\s/i
+/testost/i
+/web\sdevelopment/i
+/\slottery\s/i
+/\ssex\s/i
+/\svagina\s/i
+/\spenis\s/i
+/\smarketing\s/i
\ No newline at end of file
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_words_de.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_words_de.map
new file mode 100644
index 0000000..ccdd586
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bad_words_de.map
@@ -0,0 +1,17 @@
+/\slotto\s/i
+/pillenversand/i
+/\skredithilfe\s/i
+/\skapital\s/i
+/\skrankenversicherung\s/i
+/pädophil/i
+/paedophil/i
+/freiberufler/i
+/unternehmer/i
+/masturbieren/i
+/\sescooter\s/i
+/\se-scooter\s/i
+/testost/i
+/\spotenz\s/i
+/potenzmittel/i
+/rezeptfrei/i
+/apotheke/i
\ No newline at end of file
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bulk_header.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bulk_header.map
new file mode 100644
index 0000000..303954e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/bulk_header.map
@@ -0,0 +1,18 @@
+/X-EMV-Platform; .*/i
+/.*nur-1-click*/i
+/.*episerver.*/i
+/.*supergewinne.*/i
+/List-Unsubscribe.*nbps\.eu/i
+/X-Mailer: AWeber.*/i
+/.*regiofinder.*/i
+/.*EmailSocket.*/i
+/List-Unsubscribe:.*respread.*/i
+/.*greenflamingo.*/i
+/.*senderemailglobal.*/i
+/.*promio\.net.*/i
+/.*promio\.de.*/i
+/.*mailer-service\.com.*/i
+/.*mailer-service\.de.*/i
+/.*dynamic-lht.*/i
+/.*light-house-traffic.*/i
+/.*newsletterplus.*/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/fishy_tlds.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/fishy_tlds.map
new file mode 100644
index 0000000..1b8b2b0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/fishy_tlds.map
@@ -0,0 +1,65 @@
+/.+\.accountant$/i
+/.+\.art$/i
+/.+\.asia$/i
+/.+\.bid$/i
+/.+\.biz$/i
+/.+\.care$/i
+/.+\.cf$/i
+/.+\.click$/i
+/.+\.cloud$/i
+/.+\.co$/i
+/.+\.construction$/i
+/.+\.country$/i
+/.+\.cricket$/i
+/.+\.date$/i
+/.+\.desi$/i
+/.+\.download$/i
+/.+\.estate$/i
+/.+\.faith$/i
+/.+\.fit$/i
+/.+\.flights$/i
+/.+\.ga$/i
+/.+\.gdn$/i
+/.+\.gq$/i
+/.+\.guru$/i
+/.+\.icu$/i
+/.+\.id$/i
+/.+\.info$/i
+/.+\.in.net$/i
+/.+\.ir$/i
+/.+\.jetzt$/i
+/.+\.kim$/i
+/.+\.life$/i
+/.+\.link$/i
+/.+\.loan$/i
+/.+\.mk$/i
+/.+\.ml$/i
+/.+\.ninja$/i
+/.+\.online$/i
+/.+\.ooo$/i
+/.+\.party$/i
+/.+\.pro$/i
+/.+\.ps$/i
+/.+\.pw$/i
+/.+\.racing$/i
+/.+\.review$/i
+/.+\.rocks$/i
+/.+\.ryukyu$/i
+/.+\.science$/i
+/.+\.site$/i
+/.+\.space$/i
+/.+\.stream$/i
+/.+\.sucks$/i
+/.+\.tk$/i
+/.+\.top$/i
+/.+\.topica\.com$/i
+/.+\.town$/i
+/.+\.trade$/i
+/.+\.uno$/i
+/.+\.vip$/i
+/.+\.webcam$/i
+/.+\.website$/i
+/.+\.win$/i
+/.+\.work$/i
+/.+\.world$/i
+/.+\.xyz$/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_mime_from_blacklist.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_mime_from_blacklist.map
new file mode 100644
index 0000000..3c87288
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_mime_from_blacklist.map
@@ -0,0 +1 @@
+# /.+example\.com/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_mime_from_whitelist.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_mime_from_whitelist.map
new file mode 100644
index 0000000..3c87288
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_mime_from_whitelist.map
@@ -0,0 +1 @@
+# /.+example\.com/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_rcpt_blacklist.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_rcpt_blacklist.map
new file mode 100644
index 0000000..3c87288
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_rcpt_blacklist.map
@@ -0,0 +1 @@
+# /.+example\.com/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_rcpt_whitelist.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_rcpt_whitelist.map
new file mode 100644
index 0000000..3c87288
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_rcpt_whitelist.map
@@ -0,0 +1 @@
+# /.+example\.com/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_smtp_from_blacklist.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_smtp_from_blacklist.map
new file mode 100644
index 0000000..3c87288
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_smtp_from_blacklist.map
@@ -0,0 +1 @@
+# /.+example\.com/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_smtp_from_whitelist.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_smtp_from_whitelist.map
new file mode 100644
index 0000000..3c87288
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/global_smtp_from_whitelist.map
@@ -0,0 +1 @@
+# /.+example\.com/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/ip_wl.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/ip_wl.map
new file mode 100644
index 0000000..c8bb552
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/ip_wl.map
@@ -0,0 +1,4 @@
+# IP whitelist
+# 127.0.0.1
+# 1.2.3.4
+# ...
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/monitoring_nolog.map b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/monitoring_nolog.map
new file mode 100644
index 0000000..0e00de7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/custom/monitoring_nolog.map
@@ -0,0 +1,7 @@
+# Skip logging for these addresses
+/monitoring-system@everycloudtech\.us/i
+/monitor@tools\.mailflowmonitoring\.com/i
+/watchdog@localhost/i
+/supertool@mxtoolbox\.com/i
+/test@mxtoolboxsmtpdiag\.com/i
+/open-relay-check@mailcow\.email/i
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/aliasexp.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/aliasexp.php
new file mode 100644
index 0000000..947a024
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/aliasexp.php
@@ -0,0 +1,174 @@
+<?php
+// File size is limited by Nginx site to 10M
+// To speed things up, we do not include prerequisites
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+// Do not show errors, we log to using error_log
+ini_set('error_reporting', 0);
+// Init database
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+ PDO::ATTR_ERRMODE => 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("ALIASEXP: " . $e . PHP_EOL);
+ http_response_code(501);
+ exit;
+}
+
+// Init Redis
+$redis = new Redis();
+$redis->connect('redis-mailcow', 6379);
+
+function parse_email($email) {
+ if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
+ $a = strrpos($email, '@');
+ return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
+}
+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 rcpt
+$rcpt = $headers['Rcpt'];
+// Remove tag
+$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+// Parse email address
+$parsed_rcpt = parse_email($rcpt);
+// Create array of final mailboxes
+$rcpt_final_mailboxes = array();
+
+// Skip if not a mailcow handled domain
+try {
+ if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
+ exit;
+ }
+}
+catch (RedisException $e) {
+ error_log("ALIASEXP: " . $e . PHP_EOL);
+ http_response_code(504);
+ exit;
+}
+
+// Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
+//
+// rcpt
+// |
+// mailbox <-- goto ---> alias1, alias2, mailbox2
+// | |
+// mailbox3 |
+// |
+// alias3 ---> mailbox4
+//
+try {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(
+ ':rcpt' => $rcpt
+ ));
+ $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ if (empty($gotos)) {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(
+ ':rcpt' => '@' . $parsed_rcpt['domain']
+ ));
+ $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ }
+ if (empty($gotos)) {
+ $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+ if ($goto_branch) {
+ $gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
+ }
+ }
+ $gotos_array = explode(',', $gotos);
+
+ $loop_c = 0;
+
+ while (count($gotos_array) != 0 && $loop_c <= 20) {
+
+ // Loop through all found gotos
+ foreach ($gotos_array as $index => &$goto) {
+ error_log("ALIAS EXPANDER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
+ $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
+ $stmt->execute(array(':goto' => $goto));
+ $username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
+ if (!empty($username)) {
+ error_log("ALIAS EXPANDER: http pipe: mailbox found: " . $username . PHP_EOL);
+ // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
+ if (!in_array($username, $rcpt_final_mailboxes)) {
+ $rcpt_final_mailboxes[] = $username;
+ }
+ }
+ else {
+ $parsed_goto = parse_email($goto);
+ if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
+ error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
+ $stmt->execute(array(':goto' => $goto));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ if ($goto_branch) {
+ error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
+ $goto_branch_array = explode(',', $goto_branch);
+ } else {
+ $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
+ $stmt->execute(array(':domain' => $parsed_goto['domain']));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+ if ($goto_branch) {
+ error_log("ALIAS EXPANDER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
+ $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
+ }
+ }
+ }
+ }
+ // goto item was processed, unset
+ unset($gotos_array[$index]);
+ }
+
+ // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
+ if (!empty($goto_branch_array)) {
+ $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
+ unset($goto_branch_array);
+ }
+
+ // Reindex array
+ $gotos_array = array_values($gotos_array);
+
+ // Force exit if loop cannot be solved
+ // Postfix does not allow for alias loops, so this should never happen.
+ $loop_c++;
+ error_log("ALIAS EXPANDER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
+ }
+}
+catch (PDOException $e) {
+ error_log("ALIAS EXPANDER: " . $e->getMessage() . PHP_EOL);
+ http_response_code(502);
+ exit;
+}
+
+// Does also return the mailbox name if question == answer (query == mailbox)
+if (count($rcpt_final_mailboxes) == 1) {
+ error_log("ALIASEXP: direct alias " . $rcpt . " expanded to " . $rcpt_final_mailboxes[0] . PHP_EOL);
+ echo trim($rcpt_final_mailboxes[0]);
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/forwardinghosts.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/forwardinghosts.php
new file mode 100644
index 0000000..10285b7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/forwardinghosts.php
@@ -0,0 +1,57 @@
+<?php
+header('Content-Type: text/plain');
+ini_set('error_reporting', 0);
+
+$redis = new Redis();
+$redis->connect('redis-mailcow', 6379);
+
+function in_net($addr, $net) {
+ $net = explode('/', $net);
+ if (count($net) > 1) {
+ $mask = $net[1];
+ }
+ $net = inet_pton($net[0]);
+ $addr = inet_pton($addr);
+ $length = strlen($net); // 4 for IPv4, 16 for IPv6
+ if (strlen($net) != strlen($addr)) {
+ return false;
+ }
+ if (!isset($mask)) {
+ $mask = $length * 8;
+ }
+ $addr_bin = '';
+ $net_bin = '';
+ for ($i = 0; $i < $length; ++$i) {
+ $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT);
+ $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT);
+ }
+ return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask);
+}
+
+if (isset($_GET['host'])) {
+ try {
+ foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
+ if (in_net($_GET['host'], $host)) {
+ echo '200 PERMIT';
+ exit;
+ }
+ }
+ echo '200 DUNNO';
+ }
+ catch (RedisException $e) {
+ echo '200 DUNNO';
+ exit;
+ }
+} else {
+ try {
+ echo '240.240.240.240' . PHP_EOL;
+ foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
+ echo $host . PHP_EOL;
+ }
+ }
+ catch (RedisException $e) {
+ echo '240.240.240.240' . PHP_EOL;
+ exit;
+ }
+}
+?>
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/index.html b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/index.html
new file mode 100644
index 0000000..90531a4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/index.html
@@ -0,0 +1,2 @@
+<html>
+</html>
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/settings.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/settings.php
new file mode 100644
index 0000000..0569db9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/settings.php
@@ -0,0 +1,449 @@
+<?php
+/*
+The match section performs AND operation on different matches: for example, if you have from and rcpt in the same rule,
+then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
+then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
+*/
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+// Getting headers sent by the client.
+ini_set('error_reporting', 0);
+
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+ PDO::ATTR_ERRMODE => 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);
+ $stmt = $pdo->query("SELECT '1' FROM `filterconf`");
+}
+catch (PDOException $e) {
+ echo 'settings { }';
+ exit;
+}
+
+// Check if db changed and return header
+/*
+$stmt = $pdo->prepare("SELECT MAX(UNIX_TIMESTAMP(UPDATE_TIME)) AS `db_update_time` FROM information_schema.tables
+ WHERE (`TABLE_NAME` = 'filterconf' OR `TABLE_NAME` = 'settingsmap')
+ AND TABLE_SCHEMA = :dbname;");
+$stmt->execute(array(
+ ':dbname' => $database_name
+));
+$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time'];
+if (empty($db_update_time)) {
+ $db_update_time = 1572048000;
+}
+if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $db_update_time)) {
+ header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304);
+ exit;
+} else {
+ header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200);
+}
+*/
+
+function parse_email($email) {
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
+ $a = strrpos($email, '@');
+ return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
+}
+
+function wl_by_sogo() {
+ global $pdo;
+ $rcpt = array();
+ $stmt = $pdo->query("SELECT DISTINCT(`sogo_folder_info`.`c_path2`) AS `user`, GROUP_CONCAT(`sogo_quick_contact`.`c_mail`) AS `contacts` FROM `sogo_folder_info`
+ INNER JOIN `sogo_quick_contact` ON `sogo_quick_contact`.`c_folder_id` = `sogo_folder_info`.`c_folder_id`
+ GROUP BY `c_path2`");
+ $sogo_contacts = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($sogo_contacts)) {
+ foreach (explode(',', $row['contacts']) as $contact) {
+ if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) {
+ continue;
+ }
+ // Explicit from, no mime_from, no regex - envelope must match
+ // mailcow white and blacklists also cover mime_from
+ $rcpt[$row['user']][] = str_replace('/', '\/', $contact);
+ }
+ }
+ return $rcpt;
+}
+
+function ucl_rcpts($object, $type) {
+ global $pdo;
+ $rcpt = array();
+ if ($type == 'mailbox') {
+ // Standard aliases
+ $stmt = $pdo->prepare("SELECT `address` FROM `alias`
+ WHERE `goto` = :object_goto
+ AND `address` NOT LIKE '@%'");
+ $stmt->execute(array(
+ ':object_goto' => $object
+ ));
+ $standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($standard_aliases)) {
+ $local = parse_email($row['address'])['local'];
+ $domain = parse_email($row['address'])['domain'];
+ if (!empty($local) && !empty($domain)) {
+ $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
+ }
+ $rcpt[] = str_replace('/', '\/', $row['address']);
+ }
+ // Aliases by alias domains
+ $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
+ LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
+ WHERE `mailbox`.`username` = :object");
+ $stmt->execute(array(
+ ':object' => $object
+ ));
+ $by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ array_filter($by_domain_aliases);
+ while ($row = array_shift($by_domain_aliases)) {
+ if (!empty($row['alias'])) {
+ $local = parse_email($row['alias'])['local'];
+ $domain = parse_email($row['alias'])['domain'];
+ if (!empty($local) && !empty($domain)) {
+ $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
+ }
+ $rcpt[] = str_replace('/', '\/', $row['alias']);
+ }
+ }
+ }
+ elseif ($type == 'domain') {
+ // Domain self
+ $rcpt[] = '/.*@' . $object . '/i';
+ $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
+ WHERE `target_domain` = :object");
+ $stmt->execute(array(':object' => $object));
+ $alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ array_filter($alias_domains);
+ while ($row = array_shift($alias_domains)) {
+ $rcpt[] = '/.*@' . $row['alias_domain'] . '/i';
+ }
+ }
+ return $rcpt;
+}
+?>
+settings {
+ watchdog {
+ priority = 10;
+ rcpt_mime = "/null@localhost/i";
+ from_mime = "/watchdog@localhost/i";
+ apply "default" {
+ symbols_disabled = ["HISTORY_SAVE", "ARC", "ARC_SIGNED", "DKIM", "DKIM_SIGNED", "CLAM_VIRUS"];
+ want_spam = yes;
+ actions {
+ reject = 9999.0;
+ greylist = 9998.0;
+ "add header" = 9997.0;
+ }
+
+ }
+ }
+<?php
+
+/*
+// Start custom scores for users
+*/
+
+$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+while ($row = array_shift($rows)) {
+ $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
+?>
+ score_<?=$username_sane;?> {
+ priority = 4;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ $stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
+ WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
+ AND `object`= :object");
+ $stmt->execute(array(':object' => $row['object']));
+ $spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
+?>
+ apply "default" {
+ actions {
+ reject = <?=$spamscore['highspamlevel'][0];?>;
+ greylist = <?=$spamscore['lowspamlevel'][0] - 1;?>;
+ "add header" = <?=$spamscore['lowspamlevel'][0];?>;
+ }
+ }
+ }
+<?php
+}
+
+/*
+// Start SOGo contacts whitelist
+// Priority 4, lower than a domain whitelist (5) and lower than a mailbox whitelist (6)
+*/
+
+foreach (wl_by_sogo() as $user => $contacts) {
+ $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $user);
+?>
+ whitelist_sogo_<?=$username_sane;?> {
+<?php
+ foreach ($contacts as $contact) {
+?>
+ from = <?=json_encode($contact, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+?>
+ priority = 4;
+<?php
+ foreach (ucl_rcpts($user, 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+?>
+ apply "default" {
+ SOGO_CONTACT = -99.0;
+ }
+ symbols [
+ "SOGO_CONTACT"
+ ]
+ }
+<?php
+}
+
+/*
+// Start whitelist
+*/
+
+$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'whitelist_from'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+ $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
+?>
+ whitelist_<?=$username_sane;?> {
+<?php
+ $list_items = array();
+ $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
+ WHERE `object`= :object
+ AND `option` = 'whitelist_from'");
+ $stmt->execute(array(':object' => $row['object']));
+ $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($list_items as $item) {
+?>
+ from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
+<?php
+ }
+ if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
+?>
+ priority = 5;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+ else {
+?>
+ priority = 6;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+?>
+ apply "default" {
+ MAILCOW_WHITE = -999.0;
+ }
+ symbols [
+ "MAILCOW_WHITE"
+ ]
+ }
+ whitelist_mime_<?=$username_sane;?> {
+<?php
+ foreach ($list_items as $item) {
+?>
+ from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
+<?php
+ }
+ if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
+?>
+ priority = 5;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+ else {
+?>
+ priority = 6;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+?>
+ apply "default" {
+ MAILCOW_WHITE = -999.0;
+ }
+ symbols [
+ "MAILCOW_WHITE"
+ ]
+ }
+<?php
+}
+
+/*
+// Start blacklist
+*/
+
+$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'blacklist_from'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+ $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
+?>
+ blacklist_<?=$username_sane;?> {
+<?php
+ $list_items = array();
+ $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
+ WHERE `object`= :object
+ AND `option` = 'blacklist_from'");
+ $stmt->execute(array(':object' => $row['object']));
+ $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($list_items as $item) {
+?>
+ from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
+<?php
+ }
+ if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
+?>
+ priority = 5;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+ else {
+?>
+ priority = 6;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+?>
+ apply "default" {
+ MAILCOW_BLACK = 999.0;
+ }
+ symbols [
+ "MAILCOW_BLACK"
+ ]
+ }
+ blacklist_header_<?=$username_sane;?> {
+<?php
+ foreach ($list_items as $item) {
+?>
+ from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
+<?php
+ }
+ if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
+?>
+ priority = 5;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+ else {
+?>
+ priority = 6;
+<?php
+ foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+ }
+?>
+ apply "default" {
+ MAILCOW_BLACK = 999.0;
+ }
+ symbols [
+ "MAILCOW_BLACK"
+ ]
+ }
+<?php
+}
+
+/*
+// Start traps
+*/
+
+?>
+ ham_trap {
+<?php
+ foreach (ucl_rcpts('ham@localhost', 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+?>
+ priority = 9;
+ apply "default" {
+ symbols_enabled = ["HISTORY_SAVE"];
+ }
+ symbols [
+ "HAM_TRAP"
+ ]
+ }
+
+ spam_trap {
+<?php
+ foreach (ucl_rcpts('spam@localhost', 'mailbox') as $rcpt) {
+?>
+ rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
+<?php
+ }
+?>
+ priority = 9;
+ apply "default" {
+ symbols_enabled = ["HISTORY_SAVE"];
+ }
+ symbols [
+ "SPAM_TRAP"
+ ]
+ }
+<?php
+// Start additional content
+
+$stmt = $pdo->query("SELECT `id`, `content` FROM `settingsmap` WHERE `active` = '1'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+ $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['id']);
+?>
+ additional_settings_<?=intval($row['id']);?> {
+<?php
+ $content = preg_split('/\r\n|\r|\n/', $row['content']);
+ foreach ($content as $line) {
+ echo ' ' . $line . PHP_EOL;
+ }
+?>
+ }
+<?php
+}
+?>
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/vars.inc.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/vars.inc.php
new file mode 100644
index 0000000..79566b0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/dynmaps/vars.inc.php
@@ -0,0 +1,6 @@
+<?php
+require_once('../../../web/inc/vars.inc.php');
+if (file_exists('../../../web/inc/vars.local.inc.php')) {
+ include_once('../../../web/inc/vars.local.inc.php');
+}
+?>
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/actions.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/actions.conf
new file mode 100644
index 0000000..3de63a5
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/actions.conf
@@ -0,0 +1,3 @@
+reject = 15;
+add_header = 8;
+greylist = 7;
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/antivirus.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/antivirus.conf
new file mode 100644
index 0000000..c8d31d1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/antivirus.conf
@@ -0,0 +1,11 @@
+clamav {
+ # Scan whole message
+ scan_mime_parts = false;
+ #scan_text_mime = true;
+ #scan_image_mime = true;
+ symbol = "CLAM_VIRUS";
+ type = "clamav";
+ log_clean = true;
+ servers = "clamd:3310";
+ max_size = 20971520;
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/arc.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/arc.conf
new file mode 100644
index 0000000..a857fc4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/arc.conf
@@ -0,0 +1,32 @@
+# If false, messages with empty envelope from are not signed
+allow_envfrom_empty = true;
+# If true, envelope/header domain mismatch is ignored
+allow_hdrfrom_mismatch = true;
+# If true, multiple from headers are allowed (but only first is used)
+allow_hdrfrom_multiple = false;
+# If true, username does not need to contain matching domain
+allow_username_mismatch = false;
+# If false, messages from authenticated users are not selected for signing
+sign_authenticated = false;
+# Default path to key, can include '$domain' and '$selector' variables
+path = "/data/dkim/keys/$domain.dkim";
+# Default selector to use
+selector = "dkim";
+# If false, messages from local networks are not selected for signing
+sign_local = false;
+# Symbol to add when message is signed
+symbol = "ARC_SIGNED";
+# Whether to fallback to global config
+try_fallback = true;
+# Domain to use for DKIM signing: can be "header" or "envelope"
+use_domain = "recipient";
+# Whether to normalise domains to eSLD
+use_esld = false;
+# Whether to get keys from Redis
+use_redis = true;
+# Hash for DKIM keys in Redis
+key_prefix = "DKIM_PRIV_KEYS";
+# Selector map
+selector_prefix = "DKIM_SELECTORS";
+sign_inbound = true;
+use_domain_sign_inbound = "recipient";
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/asn.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/asn.conf
new file mode 100644
index 0000000..42b6780
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/asn.conf
@@ -0,0 +1,6 @@
+provider_type = "rspamd";
+provider_info {
+ ip4 = "asn.rspamd.com";
+ ip6 = "asn6.rspamd.com";
+}
+symbol = "ASN";
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf
new file mode 100644
index 0000000..13c977c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/composites.conf
@@ -0,0 +1,51 @@
+MX_IMPLICIT {
+ expression = "MX_GOOD & MX_MISSING";
+ score = -0.01;
+}
+VIRUS_FOUND {
+ expression = "CLAM_VIRUS & !MAILCOW_WHITE";
+ score = 2000.0;
+}
+# Bad policy from free mail providers
+FREEMAIL_POLICY_FAILURE {
+ expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
+ score = 16.0;
+}
+# Bad policy from non-whitelisted senders
+# Remove SOGO_CONTACT symbol for fwd hosts and senders with broken policy
+SOGO_CONTACT_EXCLUDE {
+ expression = "(-WHITELISTED_FWD_HOST | -g+:policies) & ^SOGO_CONTACT & !DMARC_POLICY_ALLOW";
+}
+# Spoofed header from and broken policy (excluding sieve host, rspamd host, whitelisted senders, authenticated senders and forward hosts)
+SPOOFED_UNAUTH {
+ expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & !RSPAMD_HOST & !SIEVE_HOST & MAILCOW_DOMAIN_HEADER_FROM & !WHITELISTED_FWD_HOST & -g+:policies";
+ score = 50.0;
+}
+# Only apply to inbound unauthed and not whitelisted
+OLEFY_MACRO {
+ expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & OLETOOLS";
+ score = 20.0;
+ policy = "remove_weight";
+}
+# Applies to a content filter map
+BAD_WORD_BAD_TLD {
+ expression = "FISHY_TLD & ( BAD_WORDS | BAD_WORDS_DE )"
+ score = 10.0;
+}
+# Forged with bad policies and not fwd host, keep bad policy symbols
+FORGED_W_BAD_POLICY {
+ expression = "( -g+:policies | -R_SPF_NA) & ( ~FROM_NEQ_ENVFROM | ~FORGED_SENDER ) & !WHITELISTED_FWD_HOST & !DMARC_POLICY_ALLOW"
+ score = 3.0;
+}
+# Keep negative (good) scores for rbl, policies and hfilter, disable neural group
+WL_FWD_HOST {
+ expression = "-WHITELISTED_FWD_HOST & (^g+:rbl | ^g+:policies | ^g+:hfilter | ^g:neural)"
+}
+# Exclude X-Spam like flags from scoring from fwd and sieve hosts
+UPSTREAM_CHECKS_EXCLUDE_FWD_HOST {
+ expression = "(-SIEVE_HOST | -WHITELISTED_FWD_HOST) & (^UNITEDINTERNET_SPAM | ^SPAM_FLAG | ^KLMS_SPAM | ^AOL_SPAM | ^MICROSOFT_SPAM)"
+}
+# Remove fuzzy group from bounces
+BOUNCE_FUZZY {
+ expression = "-BOUNCE & ^g+:fuzzy";
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/dkim_signing.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/dkim_signing.conf
new file mode 100644
index 0000000..13eb094
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/dkim_signing.conf
@@ -0,0 +1,35 @@
+# If false, messages with empty envelope from are not signed
+allow_envfrom_empty = true;
+# If true, envelope/header domain mismatch is ignored
+allow_hdrfrom_mismatch = true;
+# If true, multiple from headers are allowed (but only first is used)
+allow_hdrfrom_multiple = true;
+# If true, username does not need to contain matching domain
+allow_username_mismatch = true;
+# If false, messages from authenticated users are not selected for signing
+sign_authenticated = true;
+# Default path to key, can include '$domain' and '$selector' variables
+path = "/data/dkim/keys/$domain.dkim";
+# Default selector to use
+selector = "dkim";
+# If false, messages from local networks are not selected for signing
+sign_local = true;
+# Symbol to add when message is signed
+symbol = "DKIM_SIGNED";
+# Whether to fallback to global config
+try_fallback = true;
+# Domain to use for DKIM signing: can be "header" or "envelope"
+use_domain = "envelope";
+# Whether to normalise domains to eSLD
+use_esld = false;
+# Whether to get keys from Redis
+use_redis = true;
+# Hash for DKIM keys in Redis
+key_prefix = "DKIM_PRIV_KEYS";
+# Selector map
+selector_prefix = "DKIM_SELECTORS";
+# Sieve is in sign_networks only
+# forwards are arc signed, rejects are dkim signed
+sign_networks = "/etc/rspamd/custom/dovecot_trusted.map";
+use_domain_sign_networks = "header";
+sign_headers = "from:sender:reply-to:subject:date:message-id:to:cc:mime-version:content-type:content-transfer-encoding:resent-to:resent-cc:resent-from:resent-sender:resent-message-id:in-reply-to:references:list-id:list-help:list-owner:list-unsubscribe:list-subscribe:list-post:openpgp:autocrypt";
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/external_services.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/external_services.conf
new file mode 100644
index 0000000..f05314b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/external_services.conf
@@ -0,0 +1,9 @@
+oletools {
+ # default olefy settings
+ servers = "olefy:10055";
+ # needs to be set explicitly for Rspamd < 1.9.5
+ scan_mime_parts = true;
+ # mime-part regex matching in content-type or filename
+ # block all macros
+ extended = true;
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/force_actions.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/force_actions.conf
new file mode 100644
index 0000000..a1b9899
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/force_actions.conf
@@ -0,0 +1,12 @@
+rules {
+ WHITELIST_FORWARDING_HOST_NO_REJECT {
+ action = "add header";
+ expression = "WHITELISTED_FWD_HOST";
+ require_action = ["reject"];
+ }
+ WHITELIST_FORWARDING_HOST_NO_GREYLIST {
+ action = "no action";
+ expression = "WHITELISTED_FWD_HOST";
+ require_action = ["greylist", "soft reject"];
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/fuzzy_check.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/fuzzy_check.conf
new file mode 100644
index 0000000..855e8d0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/fuzzy_check.conf
@@ -0,0 +1,54 @@
+rule "local" {
+ # Fuzzy storage server list
+ servers = "localhost:11445";
+ # Default symbol for unknown flags
+ symbol = "LOCAL_FUZZY_UNKNOWN";
+ # Additional mime types to store/check
+ mime_types = ["application/*"];
+ # Hash weight threshold for all maps
+ max_score = 100.0;
+ # Whether we can learn this storage
+ read_only = no;
+ # Ignore unknown flags
+ skip_unknown = yes;
+ # Hash generation algorithm
+ algorithm = "mumhash";
+
+ # Map flags to symbols
+ fuzzy_map = {
+ LOCAL_FUZZY_DENIED {
+ max_score = 10.0;
+ flag = 11;
+ }
+ LOCAL_FUZZY_WHITE {
+ max_score = 5.0;
+ flag = 13;
+ }
+ }
+}
+
+rule "mailcow" {
+ # Fuzzy storage server list
+ servers = "fuzzy.mailcow.email:11445";
+ # Default symbol for unknown flags
+ symbol = "MAILCOW_FUZZY_UNKNOWN";
+ # Additional mime types to store/check
+ mime_types = ["application/*"];
+ # Hash weight threshold for all maps
+ max_score = 100.0;
+ # Whether we can learn this storage
+ read_only = yes;
+ # Ignore unknown flags
+ skip_unknown = yes;
+ # Hash generation algorithm
+ algorithm = "mumhash";
+ # Encrypt connection
+ encryption_key = "oa7xjgdr9u7w3hq1xbttas6brgau8qc17yi7ur5huaeq6paq8h4y";
+ # Map flags to symbols
+ fuzzy_map = {
+ MAILCOW_FUZZY_DENIED {
+ max_score = 10.0;
+ flag = 11;
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/fuzzy_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/fuzzy_group.conf
new file mode 100644
index 0000000..561ac4e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/fuzzy_group.conf
@@ -0,0 +1,17 @@
+symbols = {
+ "LOCAL_FUZZY_UNKNOWN" {
+ weight = 0.1;
+ }
+ "LOCAL_FUZZY_DENIED" {
+ weight = 15.0;
+ }
+ "MAILCOW_FUZZY_UNKNOWN" {
+ weight = 0.1;
+ }
+ "MAILCOW_FUZZY_DENIED" {
+ weight = 7.0;
+ }
+ "LOCAL_FUZZY_WHITE" {
+ weight = -10.0;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/greylist.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/greylist.conf
new file mode 100644
index 0000000..c43c907
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/greylist.conf
@@ -0,0 +1,4 @@
+whitelisted_ip = "http://nginx:8081/forwardinghosts.php";
+ipv4_mask = 24;
+ipv6_mask = 64;
+message = "Greylisted, please try again later";
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/groups.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/groups.conf
new file mode 100644
index 0000000..ef599ef
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/groups.conf
@@ -0,0 +1,50 @@
+symbols {
+ "MAILCOW_AUTH" {
+ description = "mailcow authenticated";
+ score = -20.0;
+ }
+ "CTYPE_MIXED_BOGUS" {
+ score = 0.0;
+ }
+ "BAD_REP_POLICIES" {
+ score = 2.0;
+ }
+ "BULK_HEADER" {
+ score = 4.0;
+ }
+}
+
+group "MX" {
+ "MX_INVALID" {
+ score = 0.5;
+ description = "No connectable MX";
+ one_shot = true;
+ }
+ "MX_MISSING" {
+ score = 2.0;
+ description = "No MX record";
+ one_shot = true;
+ }
+ "MX_GOOD" {
+ score = -0.01;
+ description = "MX was ok";
+ one_shot = true;
+ }
+}
+
+group "reputation" {
+ symbols = {
+ "IP_REPUTATION_HAM" {
+ weight = 1.0;
+ }
+ "IP_REPUTATION_SPAM" {
+ weight = 4.0;
+ }
+ "SENDER_REP_HAM" {
+ weight = 1.0;
+ }
+ "SENDER_REP_SPAM" {
+ weight = 2.0;
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/headers_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/headers_group.conf
new file mode 100644
index 0000000..1df92b5
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/headers_group.conf
@@ -0,0 +1,7 @@
+symbols = {
+ "R_MIXED_CHARSET" {
+ weight = 1.0;
+ description = "Mixed characters in a message";
+ one_shot = true;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/hfilter_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/hfilter_group.conf
new file mode 100644
index 0000000..3c908c5
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/hfilter_group.conf
@@ -0,0 +1,5 @@
+symbols = {
+ "HFILTER_HOSTNAME_UNKNOWN" {
+ score = 8.5;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/history_redis.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/history_redis.conf
new file mode 100644
index 0000000..68a59b0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/history_redis.conf
@@ -0,0 +1 @@
+nrows = 1000;
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/metadata_exporter.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/metadata_exporter.conf
new file mode 100644
index 0000000..f29f480
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/metadata_exporter.conf
@@ -0,0 +1,72 @@
+rules {
+ QUARANTINE {
+ backend = "http";
+ url = "http://nginx:9081/pipe.php";
+ selector = "reject_no_global_bl";
+ formatter = "default";
+ meta_headers = true;
+ }
+ RLINFO {
+ backend = "http";
+ url = "http://nginx:9081/pipe_rl.php";
+ selector = "ratelimited";
+ formatter = "json";
+ }
+ PUSHOVERMAIL {
+ backend = "http";
+ url = "http://nginx:9081/pushover.php";
+ selector = "mailcow_rcpt";
+ # Only return msgid, do not parse the full message
+ formatter = "msgid";
+ meta_headers = true;
+ }
+}
+
+custom_select {
+ mailcow_rcpt = <<EOD
+return function(task)
+ local action = task:get_metric_action('default')
+ if task:has_symbol('NO_LOG_STAT') or (action == 'reject' or action == 'add header' or action == 'rewrite subject') then
+ return false
+ else
+ if task:get_symbol("RCPT_MAILCOW_DOMAIN") then
+ return true
+ end
+ return false
+ end
+end
+EOD;
+ ratelimited = <<EOD
+return function(task)
+ local ratelimited = task:get_symbol("RATELIMITED")
+ if ratelimited then
+ return true
+ end
+ return false
+end
+EOD;
+ reject_no_global_bl = <<EOD
+return function(task)
+ if not task:has_symbol('GLOBAL_SMTP_FROM_BL')
+ and not task:has_symbol('GLOBAL_MIME_FROM_BL')
+ and not task:has_symbol('LOCAL_BL_ASN')
+ and not task:has_symbol('GLOBAL_RCPT_BL')
+ and not task:has_symbol('MAILCOW_BLACK') then
+ local action = task:get_metric_action('default')
+ if action == 'reject' or action == 'add header' or action == 'rewrite subject' then
+ return true
+ end
+ end
+ return false
+end
+EOD;
+}
+
+custom_format {
+ msgid = <<EOD
+return function(task)
+ return task:get_message_id()
+end
+EOD;
+}
+
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/milter_headers.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/milter_headers.conf
new file mode 100644
index 0000000..f61c3b9
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/milter_headers.conf
@@ -0,0 +1,43 @@
+use = ["spam-header", "x-spamd-result", "x-rspamd-queue-id", "authentication-results", "fuzzy-hashes"];
+skip_local = false;
+skip_authenticated = true;
+routines {
+ spam-header {
+ header = "X-Spam-Flag";
+ value = "YES";
+ remove = 1;
+ }
+ fuzzy-hashes {
+ header = "X-Rspamd-Fuzzy";
+ }
+ authentication-results {
+ header = "Authentication-Results";
+ add_smtp_user = false;
+ remove = 1;
+ spf_symbols {
+ pass = "R_SPF_ALLOW";
+ fail = "R_SPF_FAIL";
+ softfail = "R_SPF_SOFTFAIL";
+ neutral = "R_SPF_NEUTRAL";
+ temperror = "R_SPF_DNSFAIL";
+ none = "R_SPF_NA";
+ permerror = "R_SPF_PERMFAIL";
+ }
+ dkim_symbols {
+ pass = "R_DKIM_ALLOW";
+ fail = "R_DKIM_REJECT";
+ temperror = "R_DKIM_TEMPFAIL";
+ none = "R_DKIM_NA";
+ permerror = "R_DKIM_PERMFAIL";
+ }
+ dmarc_symbols {
+ pass = "DMARC_POLICY_ALLOW";
+ permerror = "DMARC_BAD_POLICY";
+ temperror = "DMARC_DNSFAIL";
+ none = "DMARC_NA";
+ reject = "DMARC_POLICY_REJECT";
+ softfail = "DMARC_POLICY_SOFTFAIL";
+ quarantine = "DMARC_POLICY_QUARANTINE";
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mime_types.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mime_types.conf
new file mode 100644
index 0000000..5d82982
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mime_types.conf
@@ -0,0 +1,47 @@
+# Extensions that are treated as 'bad'
+# Number is score multiply factor
+bad_extensions = {
+ scr = 20,
+ lnk = 20,
+ exe = 20,
+ msi = 1,
+ msp = 1,
+ msu = 1,
+ jar = 2,
+ com = 20,
+ bat = 4,
+ cmd = 4,
+ ps1 = 4,
+ ace = 4,
+ arj = 4,
+ cab = 3,
+ vbs = 20,
+ hta = 4,
+ shs = 4,
+ wsc = 4,
+ wsf = 4,
+ iso = 8,
+ img = 8
+};
+
+# Extensions that are particularly penalized for archives
+bad_archive_extensions = {
+ pptx = 0.5,
+ docx = 0.5,
+ xlsx = 0.5,
+ pdf = 1.0,
+ jar = 3,
+ js = 0.5,
+ vbs = 20,
+ exe = 20
+};
+
+# Used to detect another archive in archive
+archive_extensions = {
+ zip = 1,
+ arj = 1,
+ rar = 1,
+ ace = 1,
+ 7z = 1,
+ cab = 1
+};
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mime_types_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mime_types_group.conf
new file mode 100644
index 0000000..b8dae7d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mime_types_group.conf
@@ -0,0 +1,7 @@
+symbols = {
+ "MIME_DOUBLE_BAD_EXTENSION" {
+ weight = 0; # This rule has dynamic weight up to 4.0
+ description = "Bad extension cloaking";
+ one_shot = true;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf
new file mode 100644
index 0000000..0f05bb5
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/multimap.conf
@@ -0,0 +1,157 @@
+RCPT_MAILCOW_DOMAIN {
+ type = "rcpt";
+ filter = "email:domain";
+ map = "redis://DOMAIN_MAP";
+ symbols_set = ["RCPT_MAILCOW_DOMAIN"];
+}
+
+WHITELISTED_FWD_HOST {
+ type = "ip";
+ map = "redis://WHITELISTED_FWD_HOST";
+ symbols_set = ["WHITELISTED_FWD_HOST"];
+}
+
+BULK_HEADER {
+ type = "content";
+ map = "${LOCAL_CONFDIR}/custom/bulk_header.map";
+ filter = "headers"
+ regexp = true;
+ symbols_set = ["BULK_HEADER"];
+}
+
+LOCAL_BL_ASN {
+ require_symbols = "!MAILCOW_WHITE";
+ type = "asn";
+ map = "${LOCAL_CONFDIR}/custom/bad_asn.map";
+ score = 5;
+ description = "Sender's ASN is on the local blacklist";
+ symbols_set = ["LOCAL_BL_ASN"];
+}
+
+GLOBAL_SMTP_FROM_WL {
+ type = "from";
+ map = "${LOCAL_CONFDIR}/custom/global_smtp_from_whitelist.map";
+ regexp = true;
+ score = -2050;
+}
+
+GLOBAL_SMTP_FROM_BL {
+ type = "from";
+ map = "${LOCAL_CONFDIR}/custom/global_smtp_from_blacklist.map";
+ regexp = true;
+ score = 2050;
+}
+
+GLOBAL_MIME_FROM_WL {
+ type = "header";
+ header = "from";
+ filter = "email:addr";
+ map = "${LOCAL_CONFDIR}/custom/global_mime_from_whitelist.map";
+ regexp = true;
+ score = -2050;
+}
+
+GLOBAL_MIME_FROM_BL {
+ type = "header";
+ header = "from";
+ filter = "email:addr";
+ map = "${LOCAL_CONFDIR}/custom/global_mime_from_blacklist.map";
+ regexp = true;
+ score = 2050;
+}
+
+GLOBAL_RCPT_WL {
+ type = "rcpt";
+ map = "${LOCAL_CONFDIR}/custom/global_rcpt_whitelist.map";
+ regexp = true;
+ prefilter = true;
+ action = "accept";
+}
+
+GLOBAL_RCPT_BL {
+ type = "rcpt";
+ map = "${LOCAL_CONFDIR}/custom/global_rcpt_blacklist.map";
+ regexp = true;
+ prefilter = true;
+ action = "reject";
+}
+
+SIEVE_HOST {
+ type = "ip";
+ map = "${LOCAL_CONFDIR}/custom/dovecot_trusted.map";
+ symbols_set = ["SIEVE_HOST"];
+ score = -15;
+}
+
+RSPAMD_HOST {
+ type = "ip";
+ map = "${LOCAL_CONFDIR}/custom/rspamd_trusted.map";
+ symbols_set = ["RSPAMD_HOST"];
+}
+
+MAILCOW_DOMAIN_HEADER_FROM {
+ type = "header";
+ header = "from";
+ filter = "email:domain";
+ map = "redis://DOMAIN_MAP";
+}
+
+IP_WHITELIST {
+ type = "ip";
+ map = "${LOCAL_CONFDIR}/custom/ip_wl.map";
+ symbols_set = ["IP_WHITELIST"];
+ score = -2050;
+}
+
+FISHY_TLD {
+ type = "from";
+ filter = "email:domain";
+ map = "${LOCAL_CONFDIR}/custom/fishy_tlds.map";
+ regexp = true;
+ score = 0.1;
+}
+
+BAD_WORDS {
+ type = "content";
+ filter = "text";
+ map = "${LOCAL_CONFDIR}/custom/bad_words.map";
+ regexp = true;
+ score = 0.1;
+}
+
+BAD_WORDS_DE {
+ type = "content";
+ filter = "text";
+ map = "${LOCAL_CONFDIR}/custom/bad_words_de.map";
+ regexp = true;
+ score = 0.1;
+}
+
+BAD_LANG {
+ type = 'selector';
+ selector = 'languages';
+ map = "${LOCAL_CONFDIR}/custom/bad_languages.map";
+ symbols_set = ["LANG_FILTER"];
+ regexp = true;
+ score = 5.0;
+}
+
+BAZAR_ABUSE_CH {
+ type = "selector";
+ selector = "attachments(hex,md5)";
+ map = "https://bazaar.abuse.ch/export/txt/md5/recent/";
+ score = 10.0;
+}
+
+URLHAUS_ABUSE_CH {
+ type = "url";
+ filter = "full";
+ map = "https://urlhaus.abuse.ch/downloads/text_online/";
+ score = 10.0;
+}
+
+SMTP_LIMITED_ACCESS {
+ type = "user";
+ map = "redis://SMTP_LIMITED_ACCESS";
+ symbols_set = ["SMTP_LIMITED_ACCESS"];
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mx_check.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mx_check.conf
new file mode 100644
index 0000000..22fcedf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/mx_check.conf
@@ -0,0 +1,7 @@
+timeout = 8.0;
+symbol_bad_mx = "MX_INVALID";
+symbol_no_mx = "MX_MISSING";
+symbol_good_mx = "MX_GOOD";
+expire = 86400;
+key_prefix = "rmx";
+enabled = true;
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/neural.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/neural.conf
new file mode 100644
index 0000000..f4658db
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/neural.conf
@@ -0,0 +1,24 @@
+rules {
+ "LONG" {
+ train {
+ max_trains = 200;
+ max_usages = 20;
+ max_iterations = 25;
+ learning_rate = 0.01,
+ }
+ symbol_spam = "NEURAL_SPAM_LONG";
+ symbol_ham = "NEURAL_HAM_LONG";
+ ann_expire = 45d;
+ }
+ "SHORT" {
+ train {
+ max_trains = 100;
+ max_usages = 10;
+ max_iterations = 15;
+ learning_rate = 0.01,
+ }
+ symbol_spam = "NEURAL_SPAM_SHORT";
+ symbol_ham = "NEURAL_HAM_SHORT";
+ ann_expire = 7d;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/neural_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/neural_group.conf
new file mode 100644
index 0000000..fca5cec
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/neural_group.conf
@@ -0,0 +1,18 @@
+symbols = {
+ "NEURAL_SPAM_LONG" {
+ weight = 3.7; # sample weight
+ description = "Neural network spam (long)";
+ }
+ "NEURAL_HAM_LONG" {
+ weight = -4.0; # sample weight
+ description = "Neural network ham (long)";
+ }
+ "NEURAL_SPAM_SHORT" {
+ weight = 2.5; # sample weight
+ description = "Neural network spam (short)";
+ }
+ "NEURAL_HAM_SHORT" {
+ weight = -2.0; # sample weight
+ description = "Neural network ham (short)";
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/options.inc b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/options.inc
new file mode 100644
index 0000000..4fbdfba
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/options.inc
@@ -0,0 +1,9 @@
+dns {
+ enable_dnssec = true;
+}
+map_watch_interval = 30s;
+dns {
+ timeout = 4s;
+ retransmits = 2;
+}
+disable_monitoring = true;
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/phishing.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/phishing.conf
new file mode 100644
index 0000000..69be164
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/phishing.conf
@@ -0,0 +1 @@
+phishtank_enabled = false;
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/policies_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/policies_group.conf
new file mode 100644
index 0000000..8799db1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/policies_group.conf
@@ -0,0 +1,23 @@
+symbols = {
+ "ARC_REJECT" {
+ score = 0.01;
+ }
+ "R_SPF_FAIL" {
+ score = 8.0;
+ }
+ "R_SPF_PERMFAIL" {
+ score = 8.0;
+ }
+ "R_DKIM_REJECT" {
+ score = 8.0;
+ }
+ "DMARC_POLICY_REJECT" {
+ weight = 16.0;
+ }
+ "DMARC_POLICY_QUARANTINE" {
+ weight = 8.0;
+ }
+ "DMARC_POLICY_SOFTFAIL" {
+ weight = 0.0;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/rbl.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/rbl.conf
new file mode 100644
index 0000000..c44b9ef
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/rbl.conf
@@ -0,0 +1,46 @@
+rbls {
+ uceprotect1 {
+ symbol = "RBL_UCEPROTECT_LEVEL1";
+ rbl = "dnsbl-1.uceprotect.net";
+ }
+ uceprotect2 {
+ symbol = "RBL_UCEPROTECT_LEVEL2";
+ rbl = "dnsbl-2.uceprotect.net";
+ }
+ sorbs {
+ symbol = "RBL_SORBS";
+ rbl = "dnsbl.sorbs.net";
+ returncodes {
+ # http:// www.sorbs.net/general/using.shtml
+ RBL_SORBS_HTTP = "127.0.0.2";
+ RBL_SORBS_SOCKS = "127.0.0.3";
+ RBL_SORBS_MISC = "127.0.0.4";
+ RBL_SORBS_SMTP = "127.0.0.5";
+ RBL_SORBS_RECENT = "127.0.0.6";
+ RBL_SORBS_WEB = "127.0.0.7";
+ RBL_SORBS_DUL = "127.0.0.10";
+ RBL_SORBS_BLOCK = "127.0.0.8";
+ RBL_SORBS_ZOMBIE = "127.0.0.9";
+ }
+ }
+ interserver_ip {
+ symbol = "RBL_INTERSERVER_IP";
+ rbl = "rbl.interserver.net";
+ ipv6 = false;
+ returncodes {
+ RBL_INTERSERVER_BAD_IP = "127.0.0.2";
+ }
+ }
+ interserver_uri {
+ symbol = "RBL_INTERSERVER_URI";
+ rbl = "rbluri.interserver.net";
+ ignore_defaults = true;
+ no_ip = true;
+ dkim = true;
+ emails = true;
+ urls = true;
+ returncodes = {
+ RBL_INTERSERVER_BAD_URI = "127.0.0.2";
+ }
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/rbl_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/rbl_group.conf
new file mode 100644
index 0000000..4e3dce7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/rbl_group.conf
@@ -0,0 +1,60 @@
+symbols = {
+ "RBL_UCEPROTECT_LEVEL1" {
+ score = 3.5;
+ }
+ "RBL_UCEPROTECT_LEVEL2" {
+ score = 1.5;
+ }
+ "RBL_SORBS" {
+ score = 0.0;
+ description = "Unrecognised result from SORBS RBL";
+ }
+ "RBL_SORBS_HTTP" {
+ score = 2.5;
+ description = "List of Open HTTP Proxy Servers.";
+ }
+ "RBL_SORBS_SOCKS" {
+ score = 2.5;
+ description = "List of Open SOCKS Proxy Servers.";
+ }
+ "RBL_SORBS_MISC" {
+ score = 1.0;
+ description = "List of open Proxy Servers not listed in the SOCKS or HTTP lists.";
+ }
+ "RBL_SORBS_SMTP" {
+ score = 4.0;
+ description = "List of Open SMTP relay servers.";
+ }
+ "RBL_SORBS_RECENT" {
+ score = 2.0;
+ description = "List of hosts that have been noted as sending spam/UCE/UBE to the admins of SORBS within the last 28 days (includes new.spam.dnsbl.sorbs.net).";
+ }
+ "RBL_SORBS_WEB" {
+ score = 2.0;
+ description = "List of web (WWW) servers which have spammer abusable vulnerabilities (e.g. FormMail scripts)";
+ }
+ "RBL_SORBS_DUL" {
+ score = 2.0;
+ description = "Dynamic IP Address ranges (NOT a Dial Up list!)";
+ }
+ "RBL_SORBS_BLOCK" {
+ score = 0.5;
+ description = "List of hosts demanding that they never be tested by SORBS.";
+ }
+ "RBL_SORBS_ZOMBIE" {
+ score = 2.0;
+ description = "List of networks hijacked from their original owners, some of which have already used for spamming.";
+ }
+ "RECEIVED_SPAMHAUS_XBL" {
+ weight = 0.0;
+ description = "Received address is listed in ZEN XBL";
+ }
+ "RBL_INTERSERVER_BAD_URI" {
+ score = 4.0;
+ description = "Listed on Interserver RBL";
+ }
+ "RBL_INTERSERVER_BAD_IP" {
+ score = 4.0;
+ description = "Listed on Interserver RBL";
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/redis.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/redis.conf
new file mode 100644
index 0000000..5ee0ac1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/redis.conf
@@ -0,0 +1,2 @@
+servers = "redis:6379";
+timeout = 10;
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/reputation.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/reputation.conf
new file mode 100644
index 0000000..0e3d03e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/reputation.conf
@@ -0,0 +1,10 @@
+rules {
+ ip_reputation = {
+ selector "ip" {
+ }
+ backend "redis" {
+ servers = "redis";
+ }
+ symbol = "IP_REPUTATION";
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/spamassassin.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/spamassassin.conf
new file mode 100644
index 0000000..d091af6
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/spamassassin.conf
@@ -0,0 +1 @@
+ruleset = "/etc/rspamd/custom/sa-rules";
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/statistic.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/statistic.conf
new file mode 100644
index 0000000..1ca3e08
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/statistic.conf
@@ -0,0 +1,24 @@
+classifier "bayes" {
+ tokenizer {
+ name = "osb";
+ }
+ backend = "redis";
+ min_tokens = 11;
+ min_learns = 5;
+ new_schema = true;
+ expire = 2592000;
+ statfile {
+ symbol = "BAYES_HAM";
+ spam = false;
+ }
+ statfile {
+ symbol = "BAYES_SPAM";
+ spam = true;
+ }
+ autolearn {
+ spam_threshold = 12.0;
+ ham_threshold = -4.5;
+ check_balance = true;
+ min_balance = 0.9;
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/statistics_group.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/statistics_group.conf
new file mode 100644
index 0000000..7ed35b1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/local.d/statistics_group.conf
@@ -0,0 +1,10 @@
+symbols = {
+ "BAYES_SPAM" {
+ weight = 2.5;
+ description = "Message probably spam, probability: ";
+ }
+ "BAYES_HAM" {
+ weight = -5.5;
+ description = "Message probably ham, probability: ";
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/ivm-sg.lua b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/ivm-sg.lua
new file mode 100644
index 0000000..6642fe4
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/ivm-sg.lua
@@ -0,0 +1,61 @@
+-- Thanks to https://raw.githubusercontent.com/fatalbanana
+
+local lua_maps = require 'lua_maps'
+local rspamd_regexp = require 'rspamd_regexp'
+local rspamd_util = require 'rspamd_util'
+
+local ivm_sendgrid_ids = lua_maps.map_add_from_ucl(
+ 'https://www.invaluement.com/spdata/sendgrid-id-dnsbl.txt',
+ 'set',
+ 'Invaluement Service Provider DNSBL: Sendgrid IDs'
+)
+
+local ivm_sendgrid_envfromdomains = lua_maps.map_add_from_ucl(
+ 'https://www.invaluement.com/spdata/sendgrid-envelopefromdomain-dnsbl.txt',
+ 'set',
+ 'Invaluement Service Provider DNSBL: Sendgrid envelope domains'
+)
+
+local cb_id = rspamd_config:register_symbol({
+ name = 'IVM_SENDGRID',
+ callback = function(task)
+ -- Is it Sendgrid?
+ local sg_hdr = task:get_header('X-SG-EID')
+ if not sg_hdr then return end
+
+ -- Get original envelope from
+ local env_from = task:get_from{'smtp', 'orig'}
+ if not env_from then return end
+
+ -- Check normalised domain in domains list
+ if ivm_sendgrid_envfromdomains and ivm_sendgrid_envfromdomains:get_key(rspamd_util.get_tld(env_from[1].domain)) then
+ task:insert_result('IVM_SENDGRID_DOMAIN', 1.0)
+ end
+
+ -- Check ID in ID list
+ local lp_re = rspamd_regexp.create_cached([[^bounces\+(\d+)-]])
+ local res = lp_re:search(env_from[1].user, true, true)
+ if not res then return end
+ if ivm_sendgrid_ids and ivm_sendgrid_ids:get_key(res[1][2]) then
+ task:insert_result('IVM_SENDGRID_ID', 1.0)
+ end
+ end,
+ description = 'Invaluement Service Provider DNSBL: Sendgrid',
+ type = 'callback',
+})
+
+rspamd_config:register_symbol({
+ name = 'IVM_SENDGRID_DOMAIN',
+ parent = cb_id,
+ group = 'ivmspdnsbl',
+ score = 8.0,
+ type = 'virtual',
+})
+
+rspamd_config:register_symbol({
+ name = 'IVM_SENDGRID_ID',
+ parent = cb_id,
+ group = 'ivmspdnsbl',
+ score = 8.0,
+ type = 'virtual',
+})
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/ratelimit.lua b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/ratelimit.lua
new file mode 100644
index 0000000..635fe3e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/ratelimit.lua
@@ -0,0 +1,16 @@
+local custom_keywords = {}
+
+custom_keywords.mailcow = function(task)
+ local rspamd_logger = require "rspamd_logger"
+ local dyn_rl_symbol = task:get_symbol("DYN_RL")
+ if dyn_rl_symbol then
+ local rl_value = dyn_rl_symbol[1].options[1]
+ local rl_object = dyn_rl_symbol[1].options[2]
+ if rl_value and rl_object then
+ rspamd_logger.infox(rspamd_config, "DYN_RL symbol has value %s for object %s, returning %s...", rl_value, rl_object, "rs_dynrl_" .. rl_object)
+ return "rs_dynrl_" .. rl_object, rl_value
+ end
+ end
+end
+
+return custom_keywords
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua
new file mode 100644
index 0000000..3f4c326
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/lua/rspamd.local.lua
@@ -0,0 +1,398 @@
+-- Load sendgrid ID validator, thanks to https://github.com/fatalbanana
+local rspamd_util = require 'rspamd_util'
+local f = '/etc/rspamd/lua/ivm-sg.lua'
+if rspamd_util.file_exists(f) then
+ dofile(f)
+end
+
+rspamd_config.MAILCOW_AUTH = {
+ callback = function(task)
+ local uname = task:get_user()
+ if uname then
+ return 1
+ end
+ end
+}
+
+local monitoring_hosts = rspamd_config:add_map{
+ url = "/etc/rspamd/custom/monitoring_nolog.map",
+ description = "Monitoring hosts",
+ type = "regexp"
+}
+
+rspamd_config:register_symbol({
+ name = 'SMTP_ACCESS',
+ type = 'postfilter',
+ callback = function(task)
+ local util = require("rspamd_util")
+ local rspamd_logger = require "rspamd_logger"
+ local rspamd_ip = require 'rspamd_ip'
+ local uname = task:get_user()
+ local limited_access = task:get_symbol("SMTP_LIMITED_ACCESS")
+
+ if not uname then
+ return false
+ end
+
+ if not limited_access then
+ return false
+ end
+
+ local hash_key = 'SMTP_ALLOW_NETS_' .. uname
+
+ local redis_params = rspamd_parse_redis_server('smtp_access')
+ local ip = task:get_from_ip()
+
+ if ip == nil or not ip:is_valid() then
+ return false
+ end
+
+ local from_ip_string = tostring(ip)
+ smtp_access_table = {from_ip_string}
+
+ local maxbits = 128
+ local minbits = 32
+ if ip:get_version() == 4 then
+ maxbits = 32
+ minbits = 8
+ end
+ for i=maxbits,minbits,-1 do
+ local nip = ip:apply_mask(i):to_string() .. "/" .. i
+ table.insert(smtp_access_table, nip)
+ end
+ local function smtp_access_cb(err, data)
+ if err then
+ rspamd_logger.infox(rspamd_config, "smtp_access query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
+ return false
+ else
+ rspamd_logger.infox(rspamd_config, "checking ip %s for smtp_access in %s", from_ip_string, hash_key)
+ for k,v in pairs(data) do
+ if (v and v ~= userdata and v == '1') then
+ rspamd_logger.infox(rspamd_config, "found ip in smtp_access map")
+ task:insert_result(true, 'SMTP_ACCESS', 0.0, from_ip_string)
+ return true
+ end
+ end
+ rspamd_logger.infox(rspamd_config, "couldnt find ip in smtp_access map")
+ task:insert_result(true, 'SMTP_ACCESS', 999.0, from_ip_string)
+ return true
+ end
+ end
+ table.insert(smtp_access_table, 1, hash_key)
+ local redis_ret_user = rspamd_redis_make_request(task,
+ redis_params, -- connect params
+ hash_key, -- hash key
+ false, -- is write
+ smtp_access_cb, --callback
+ 'HMGET', -- command
+ smtp_access_table -- arguments
+ )
+ if not redis_ret_user then
+ rspamd_logger.infox(rspamd_config, "cannot check smtp_access redis map")
+ end
+ end,
+ priority = 10
+})
+
+rspamd_config:register_symbol({
+ name = 'POSTMASTER_HANDLER',
+ type = 'prefilter',
+ callback = function(task)
+ local rcpts = task:get_recipients('smtp')
+ local rspamd_logger = require "rspamd_logger"
+ local lua_util = require "lua_util"
+ local from = task:get_from(1)
+
+ -- not applying to mails with more than one rcpt to avoid bypassing filters by addressing postmaster
+ if rcpts and #rcpts == 1 then
+ for _,rcpt in ipairs(rcpts) do
+ local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
+ if #rcpt_split == 2 then
+ if rcpt_split[1] == 'postmaster' then
+ task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
+ return
+ end
+ end
+ end
+ end
+
+ if from then
+ for _,fr in ipairs(from) do
+ local fr_split = rspamd_str_split(fr['addr'], '@')
+ if #fr_split == 2 then
+ if fr_split[1] == 'postmaster' and task:get_user() then
+ -- no whitelist, keep signatures
+ task:insert_result(true, 'POSTMASTER_FROM', -2500.0)
+ return
+ end
+ end
+ end
+ end
+
+ end,
+ priority = 10
+})
+
+rspamd_config:register_symbol({
+ name = 'KEEP_SPAM',
+ type = 'prefilter',
+ callback = function(task)
+ local util = require("rspamd_util")
+ local rspamd_logger = require "rspamd_logger"
+ local rspamd_ip = require 'rspamd_ip'
+ local uname = task:get_user()
+
+ if uname then
+ return false
+ end
+
+ local redis_params = rspamd_parse_redis_server('keep_spam')
+ local ip = task:get_from_ip()
+
+ if ip == nil or not ip:is_valid() then
+ return false
+ end
+
+ local from_ip_string = tostring(ip)
+ ip_check_table = {from_ip_string}
+
+ local maxbits = 128
+ local minbits = 32
+ if ip:get_version() == 4 then
+ maxbits = 32
+ minbits = 8
+ end
+ for i=maxbits,minbits,-1 do
+ local nip = ip:apply_mask(i):to_string() .. "/" .. i
+ table.insert(ip_check_table, nip)
+ end
+ local function keep_spam_cb(err, data)
+ if err then
+ rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
+ return false
+ else
+ for k,v in pairs(data) do
+ if (v and v ~= userdata and v == '1') then
+ rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
+ task:set_pre_result('accept', 'ip matched with forward hosts')
+ end
+ end
+ end
+ end
+ table.insert(ip_check_table, 1, 'KEEP_SPAM')
+ local redis_ret_user = rspamd_redis_make_request(task,
+ redis_params, -- connect params
+ 'KEEP_SPAM', -- hash key
+ false, -- is write
+ keep_spam_cb, --callback
+ 'HMGET', -- command
+ ip_check_table -- arguments
+ )
+ if not redis_ret_user then
+ rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map")
+ end
+ end,
+ priority = 19
+})
+
+rspamd_config:register_symbol({
+ name = 'TLS_HEADER',
+ type = 'postfilter',
+ callback = function(task)
+ local rspamd_logger = require "rspamd_logger"
+ local tls_tag = task:get_request_header('TLS-Version')
+ if type(tls_tag) == 'nil' then
+ task:set_milter_reply({
+ add_headers = {['X-Last-TLS-Session-Version'] = 'None'}
+ })
+ else
+ task:set_milter_reply({
+ add_headers = {['X-Last-TLS-Session-Version'] = tostring(tls_tag)}
+ })
+ end
+ end,
+ priority = 12
+})
+
+rspamd_config:register_symbol({
+ name = 'TAG_MOO',
+ type = 'postfilter',
+ callback = function(task)
+ local util = require("rspamd_util")
+ local rspamd_logger = require "rspamd_logger"
+ local redis_params = rspamd_parse_redis_server('taghandler')
+ local rspamd_http = require "rspamd_http"
+ local rcpts = task:get_recipients('smtp')
+ local lua_util = require "lua_util"
+
+ local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
+ local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
+
+ if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
+ local tag = tagged_rcpt[1].options[1]
+ rspamd_logger.infox("found tag: %s", tag)
+ local action = task:get_metric_action('default')
+ rspamd_logger.infox("metric action now: %s", action)
+
+ if action ~= 'no action' and action ~= 'greylist' then
+ rspamd_logger.infox("skipping tag handler for action: %s", action)
+ return true
+ end
+
+ local function http_callback(err_message, code, body, headers)
+ if body ~= nil and body ~= "" then
+ rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
+
+ local function tag_callback_subject(err, data)
+ if err or type(data) ~= 'string' then
+ rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
+
+ local function tag_callback_subfolder(err, data)
+ if err or type(data) ~= 'string' then
+ rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
+ else
+ rspamd_logger.infox("Add X-Moo-Tag header")
+ task:set_milter_reply({
+ add_headers = {['X-Moo-Tag'] = 'YES'}
+ })
+ end
+ end
+
+ local redis_ret_subfolder = rspamd_redis_make_request(task,
+ redis_params, -- connect params
+ body, -- hash key
+ false, -- is write
+ tag_callback_subfolder, --callback
+ 'HGET', -- command
+ {'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
+ )
+ if not redis_ret_subfolder then
+ rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
+ end
+
+ else
+ rspamd_logger.infox("user wants subject modified for tagged mail")
+ local sbj = task:get_header('Subject')
+ new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
+ task:set_milter_reply({
+ remove_headers = {['Subject'] = 1},
+ add_headers = {['Subject'] = new_sbj}
+ })
+ end
+ end
+
+ local redis_ret_subject = rspamd_redis_make_request(task,
+ redis_params, -- connect params
+ body, -- hash key
+ false, -- is write
+ tag_callback_subject, --callback
+ 'HGET', -- command
+ {'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
+ )
+ if not redis_ret_subject then
+ rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
+ end
+
+ end
+ end
+
+ if rcpts and #rcpts == 1 then
+ for _,rcpt in ipairs(rcpts) do
+ local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
+ if #rcpt_split == 2 then
+ if rcpt_split[1] == 'postmaster' then
+ rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
+ else
+ rspamd_http.request({
+ task=task,
+ url='http://nginx:8081/aliasexp.php',
+ body='',
+ callback=http_callback,
+ headers={Rcpt=rcpt['addr']},
+ })
+ end
+ end
+ end
+ end
+
+ end
+ end,
+ priority = 19
+})
+
+rspamd_config:register_symbol({
+ name = 'DYN_RL_CHECK',
+ type = 'prefilter',
+ callback = function(task)
+ local util = require("rspamd_util")
+ local redis_params = rspamd_parse_redis_server('dyn_rl')
+ local rspamd_logger = require "rspamd_logger"
+ 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 redis_cb_user(err, data)
+
+ if err or type(data) ~= 'string' then
+ rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for user %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying dynamic ratelimit for domain...", uname, data, err)
+
+ local function redis_key_cb_domain(err, data)
+ if err or type(data) ~= 'string' then
+ rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for domain %s returned invalid or empty data (\"%s\") or error (\"%s\")", env_from_domain, data, err)
+ else
+ rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for domain %s with value %s", env_from_domain, data)
+ task:insert_result('DYN_RL', 0.0, data, env_from_domain)
+ end
+ end
+
+ local redis_ret_domain = rspamd_redis_make_request(task,
+ redis_params, -- connect params
+ env_from_domain, -- hash key
+ false, -- is write
+ redis_key_cb_domain, --callback
+ 'HGET', -- command
+ {'RL_VALUE', env_from_domain} -- arguments
+ )
+ if not redis_ret_domain then
+ rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for domain")
+ end
+ else
+ rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for user %s with value %s", uname, data)
+ task:insert_result('DYN_RL', 0.0, data, uname)
+ end
+
+ end
+
+ local redis_ret_user = rspamd_redis_make_request(task,
+ redis_params, -- connect params
+ uname, -- hash key
+ false, -- is write
+ redis_cb_user, --callback
+ 'HGET', -- command
+ {'RL_VALUE', uname} -- arguments
+ )
+ if not redis_ret_user then
+ rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for user")
+ end
+ return true
+ end,
+ flags = 'empty',
+ priority = 20
+})
+
+rspamd_config:register_symbol({
+ name = 'NO_LOG_STAT',
+ type = 'postfilter',
+ callback = function(task)
+ local from = task:get_header('From')
+ if from and monitoring_hosts:get_key(from) then
+ task:set_flag('no_log')
+ task:set_flag('no_stat')
+ end
+ end
+})
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pipe.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pipe.php
new file mode 100644
index 0000000..88e66e8
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pipe.php
@@ -0,0 +1,260 @@
+<?php
+// File size is limited by Nginx site to 10M
+// To speed things up, we do not include prerequisites
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+// Do not show errors, we log to using error_log
+ini_set('error_reporting', 0);
+// Init database
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+ PDO::ATTR_ERRMODE => 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("QUARANTINE: " . $e . PHP_EOL);
+ http_response_code(501);
+ exit;
+}
+// Init Redis
+$redis = new Redis();
+$redis->connect('redis-mailcow', 6379);
+
+// Functions
+function parse_email($email) {
+ if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
+ $a = strrpos($email, '@');
+ return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
+}
+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;
+ }
+}
+
+$raw_data_content = file_get_contents('php://input');
+$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
+$headers = getallheaders();
+
+$qid = $headers['X-Rspamd-Qid'];
+$fuzzy = $headers['X-Rspamd-Fuzzy'];
+$subject = $headers['X-Rspamd-Subject'];
+$score = $headers['X-Rspamd-Score'];
+$rcpts = $headers['X-Rspamd-Rcpt'];
+$user = $headers['X-Rspamd-User'];
+$ip = $headers['X-Rspamd-Ip'];
+$action = $headers['X-Rspamd-Action'];
+$sender = $headers['X-Rspamd-From'];
+$symbols = $headers['X-Rspamd-Symbols'];
+
+$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
+
+if (empty($sender)) {
+ error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
+ $sender = 'empty-env-from@localhost';
+}
+
+if ($fuzzy == 'unknown') {
+ $fuzzy = '[]';
+}
+
+try {
+ $max_size = (int)$redis->Get('Q_MAX_SIZE');
+ if (($max_size * 1048576) < $raw_size) {
+ error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL);
+ http_response_code(505);
+ exit;
+ }
+ if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) {
+ $exclude_domains = json_decode($exclude_domains, true);
+ }
+ $retention_size = (int)$redis->Get('Q_RETENTION_SIZE');
+}
+catch (RedisException $e) {
+ error_log("QUARANTINE: " . $e . PHP_EOL);
+ http_response_code(504);
+ exit;
+}
+
+$rcpt_final_mailboxes = array();
+
+// Loop through all rcpts
+foreach (json_decode($rcpts, true) as $rcpt) {
+ // Remove tag
+ $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+
+ // Break rcpt into local part and domain part
+ $parsed_rcpt = parse_email($rcpt);
+
+ // Skip if not a mailcow handled domain
+ try {
+ if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
+ continue;
+ }
+ }
+ catch (RedisException $e) {
+ error_log("QUARANTINE: " . $e . PHP_EOL);
+ http_response_code(504);
+ exit;
+ }
+
+ // Skip if domain is excluded
+ if (in_array($parsed_rcpt['domain'], $exclude_domains)) {
+ error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL);
+ continue;
+ }
+
+ // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
+ //
+ // rcpt
+ // |
+ // mailbox <-- goto ---> alias1, alias2, mailbox2
+ // | |
+ // mailbox3 |
+ // |
+ // alias3 ---> mailbox4
+ //
+ try {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(
+ ':rcpt' => $rcpt
+ ));
+ $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ if (empty($gotos)) {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(
+ ':rcpt' => '@' . $parsed_rcpt['domain']
+ ));
+ $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ }
+ if (empty($gotos)) {
+ $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+ if ($goto_branch) {
+ $gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
+ }
+ }
+ $gotos_array = explode(',', $gotos);
+
+ $loop_c = 0;
+
+ while (count($gotos_array) != 0 && $loop_c <= 20) {
+
+ // Loop through all found gotos
+ foreach ($gotos_array as $index => &$goto) {
+ error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
+ $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
+ $stmt->execute(array(':goto' => $goto));
+ $username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
+ if (!empty($username)) {
+ error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
+ // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
+ if (!in_array($username, $rcpt_final_mailboxes)) {
+ $rcpt_final_mailboxes[] = $username;
+ }
+ }
+ else {
+ $parsed_goto = parse_email($goto);
+ if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
+ error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
+ $stmt->execute(array(':goto' => $goto));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ if ($goto_branch) {
+ error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
+ $goto_branch_array = explode(',', $goto_branch);
+ } else {
+ $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
+ $stmt->execute(array(':domain' => $parsed_goto['domain']));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+ if ($goto_branch) {
+ error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
+ $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
+ }
+ }
+ }
+ }
+ // goto item was processed, unset
+ unset($gotos_array[$index]);
+ }
+
+ // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
+ if (!empty($goto_branch_array)) {
+ $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
+ unset($goto_branch_array);
+ }
+
+ // Reindex array
+ $gotos_array = array_values($gotos_array);
+
+ // Force exit if loop cannot be solved
+ // Postfix does not allow for alias loops, so this should never happen.
+ $loop_c++;
+ error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
+ }
+ }
+ catch (PDOException $e) {
+ error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
+ http_response_code(502);
+ exit;
+ }
+}
+
+foreach ($rcpt_final_mailboxes as $rcpt_final) {
+ error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt_final . PHP_EOL);
+ try {
+ $stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`)
+ VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes)");
+ $stmt->execute(array(
+ ':qid' => $qid,
+ ':subject' => $subject,
+ ':score' => $score,
+ ':sender' => $sender,
+ ':rcpt' => $rcpt_final,
+ ':symbols' => $symbols,
+ ':user' => $user,
+ ':ip' => $ip,
+ ':msg' => $raw_data,
+ ':action' => $action,
+ ':fuzzy_hashes' => $fuzzy
+ ));
+ $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
+ SELECT `id`
+ FROM (
+ SELECT `id`
+ FROM `quarantine`
+ WHERE `rcpt` = :rcpt2
+ ORDER BY id DESC
+ LIMIT :retention_size
+ ) x
+ );');
+ $stmt->execute(array(
+ ':rcpt' => $rcpt_final,
+ ':rcpt2' => $rcpt_final,
+ ':retention_size' => $retention_size
+ ));
+ }
+ catch (PDOException $e) {
+ error_log("QUARANTINE: " . $e->getMessage() . PHP_EOL);
+ http_response_code(503);
+ exit;
+ }
+}
+
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pipe_rl.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pipe_rl.php
new file mode 100644
index 0000000..5f7fd42
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pipe_rl.php
@@ -0,0 +1,48 @@
+<?php
+// File size is limited by Nginx site to 10M
+// To speed things up, we do not include prerequisites
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+// Do not show errors, we log to using error_log
+ini_set('error_reporting', 0);
+// Init Redis
+$redis = new Redis();
+try {
+ if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
+ $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
+ }
+ else {
+ $redis->connect('redis-mailcow', 6379);
+ }
+}
+catch (Exception $e) {
+ exit;
+}
+
+$raw_data_content = file_get_contents('php://input');
+$raw_data_decoded = json_decode($raw_data_content, true);
+
+$data['time'] = time();
+$data['rcpt'] = implode(', ', $raw_data_decoded['rcpt']);
+$data['from'] = $raw_data_decoded['from'];
+$data['user'] = $raw_data_decoded['user'];
+$symbol_rl_key = array_search('RATELIMITED', array_column($raw_data_decoded['symbols'], 'name'));
+$data['rl_info'] = implode($raw_data_decoded['symbols'][$symbol_rl_key]['options']);
+preg_match('/(.+)\((.+)\)/i', $data['rl_info'], $rl_matches);
+if (!empty($rl_matches[1]) && !empty($rl_matches[2])) {
+ $data['rl_name'] = $rl_matches[1];
+ $data['rl_hash'] = $rl_matches[2];
+}
+else {
+ $data['rl_name'] = 'err';
+ $data['rl_hash'] = 'err';
+}
+$data['qid'] = $raw_data_decoded['qid'];
+$data['ip'] = $raw_data_decoded['ip'];
+$data['message_id'] = $raw_data_decoded['message_id'];
+$data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']);
+$data['header_from'] = implode(', ', $raw_data_decoded['header_from']);
+
+$redis->lpush('RL_LOG', json_encode($data));
+exit;
+
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pushover.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pushover.php
new file mode 100644
index 0000000..a5e8334
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/pushover.php
@@ -0,0 +1,252 @@
+<?php
+// File size is limited by Nginx site to 10M
+// To speed things up, we do not include prerequisites
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+// Do not show errors, we log to using error_log
+ini_set('error_reporting', 0);
+// Init database
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+ PDO::ATTR_ERRMODE => 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("NOTIFY: " . $e . PHP_EOL);
+ http_response_code(501);
+ exit;
+}
+// Init Redis
+$redis = new Redis();
+$redis->connect('redis-mailcow', 6379);
+
+// Functions
+function parse_email($email) {
+ if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
+ $a = strrpos($email, '@');
+ return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
+}
+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;
+ }
+}
+
+$headers = getallheaders();
+
+$qid = $headers['X-Rspamd-Qid'];
+$rcpts = $headers['X-Rspamd-Rcpt'];
+$sender = $headers['X-Rspamd-From'];
+$ip = $headers['X-Rspamd-Ip'];
+$subject = $headers['X-Rspamd-Subject'];
+$priority = 0;
+
+$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
+if (is_array($symbols_array)) {
+ foreach ($symbols_array as $symbol) {
+ if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
+ $priority = 1;
+ break;
+ }
+ }
+}
+
+$rcpt_final_mailboxes = array();
+
+// Loop through all rcpts
+foreach (json_decode($rcpts, true) as $rcpt) {
+ // Remove tag
+ $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+
+ // Break rcpt into local part and domain part
+ $parsed_rcpt = parse_email($rcpt);
+
+ // Skip if not a mailcow handled domain
+ try {
+ if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
+ continue;
+ }
+ }
+ catch (RedisException $e) {
+ error_log("NOTIFY: " . $e . PHP_EOL);
+ http_response_code(504);
+ exit;
+ }
+
+ // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
+ //
+ // rcpt
+ // |
+ // mailbox <-- goto ---> alias1, alias2, mailbox2
+ // | |
+ // mailbox3 |
+ // |
+ // alias3 ---> mailbox4
+ //
+ try {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(
+ ':rcpt' => $rcpt
+ ));
+ $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ if (empty($gotos)) {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(
+ ':rcpt' => '@' . $parsed_rcpt['domain']
+ ));
+ $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ }
+ if (empty($gotos)) {
+ $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
+ $stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+ if ($goto_branch) {
+ $gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
+ }
+ }
+ $gotos_array = explode(',', $gotos);
+
+ $loop_c = 0;
+
+ while (count($gotos_array) != 0 && $loop_c <= 20) {
+
+ // Loop through all found gotos
+ foreach ($gotos_array as $index => &$goto) {
+ error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
+ $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
+ $stmt->execute(array(':goto' => $goto));
+ $username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
+ if (!empty($username)) {
+ error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
+ // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
+ if (!in_array($username, $rcpt_final_mailboxes)) {
+ $rcpt_final_mailboxes[] = $username;
+ }
+ }
+ else {
+ $parsed_goto = parse_email($goto);
+ if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
+ error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
+ $stmt->execute(array(':goto' => $goto));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+ if ($goto_branch) {
+ error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
+ $goto_branch_array = explode(',', $goto_branch);
+ } else {
+ $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
+ $stmt->execute(array(':domain' => $parsed_goto['domain']));
+ $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+ if ($goto_branch) {
+ error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
+ $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
+ }
+ }
+ }
+ }
+ // goto item was processed, unset
+ unset($gotos_array[$index]);
+ }
+
+ // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
+ if (!empty($goto_branch_array)) {
+ $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
+ unset($goto_branch_array);
+ }
+
+ // Reindex array
+ $gotos_array = array_values($gotos_array);
+
+ // Force exit if loop cannot be solved
+ // Postfix does not allow for alias loops, so this should never happen.
+ $loop_c++;
+ error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
+ }
+ }
+ catch (PDOException $e) {
+ error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
+ http_response_code(502);
+ exit;
+ }
+}
+
+
+foreach ($rcpt_final_mailboxes as $rcpt_final) {
+ error_log("NOTIFY: pushover pipe: processing pushover message for rcpt " . $rcpt_final . PHP_EOL);
+ $stmt = $pdo->prepare("SELECT * FROM `pushover`
+ WHERE `username` = :username AND `active` = '1'");
+ $stmt->execute(array(
+ ':username' => $rcpt_final
+ ));
+ $api_data = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (isset($api_data['key']) && isset($api_data['token'])) {
+ $title = (!empty($api_data['title'])) ? $api_data['title'] : 'Mail';
+ $text = (!empty($api_data['text'])) ? $api_data['text'] : 'You\'ve got mail 📧';
+ $attributes = json_decode($api_data['attributes'], true);
+ $senders = explode(',', $api_data['senders']);
+ $senders = array_filter($senders);
+ $senders_regex = $api_data['senders_regex'];
+ $sender_validated = false;
+ if (empty($senders) && empty($senders_regex)) {
+ $sender_validated = true;
+ }
+ else {
+ if (!empty($senders)) {
+ if (in_array($sender, $senders)) {
+ $sender_validated = true;
+ }
+ }
+ if (!empty($senders_regex) && $sender_validated !== true) {
+ if (preg_match($senders_regex, $sender)) {
+ $sender_validated = true;
+ }
+ }
+ }
+ if ($sender_validated === false) {
+ error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender);
+ continue;
+ }
+ if ($attributes['only_x_prio'] == "1" && $priority == 0) {
+ error_log("NOTIFY: pushover pipe: mail has no X-Priority: 1 header, skipping");
+ continue;
+ }
+ $post_fields = array(
+ "token" => $api_data['token'],
+ "user" => $api_data['key'],
+ "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $title)),
+ "priority" => $priority,
+ "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $text))
+ );
+ if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) {
+ $post_fields['expire'] = 600;
+ $post_fields['retry'] = 120;
+ $post_fields['priority'] = 2;
+ }
+ curl_setopt_array($ch = curl_init(), array(
+ CURLOPT_URL => "https://api.pushover.net/1/messages.json",
+ CURLOPT_POSTFIELDS => $post_fields,
+ CURLOPT_SAFE_UPLOAD => true,
+ CURLOPT_RETURNTRANSFER => true,
+ ));
+ $result = curl_exec($ch);
+ $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ error_log("NOTIFY: result: " . $httpcode . PHP_EOL);
+ }
+}
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/vars.inc.php b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/vars.inc.php
new file mode 100644
index 0000000..79566b0
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/meta_exporter/vars.inc.php
@@ -0,0 +1,6 @@
+<?php
+require_once('../../../web/inc/vars.inc.php');
+if (file_exists('../../../web/inc/vars.local.inc.php')) {
+ include_once('../../../web/inc/vars.local.inc.php');
+}
+?>
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/logging.inc b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/logging.inc
new file mode 100644
index 0000000..64d4064
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/logging.inc
@@ -0,0 +1,5 @@
+level = "silent";
+type = "console";
+systemd = false;
+.include "$CONFDIR/logging.inc"
+.include(try=true; priority=20) "$CONFDIR/override.d/logging.custom.inc"
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/ratelimit.conf b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/ratelimit.conf
new file mode 100644
index 0000000..aec1c78
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/ratelimit.conf
@@ -0,0 +1,12 @@
+rates {
+ # Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default
+ to = "100 / 1s";
+ to_ip = "100 / 1s";
+ to_ip_from = "100 / 1s";
+ bounce_to = "100 / 1h";
+ bounce_to_ip = "7 / 1m";
+}
+whitelisted_rcpts = "postmaster,mailer-daemon";
+max_rcpt = 25;
+custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
+info_symbol = "RATELIMITED";
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-controller.inc b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-controller.inc
new file mode 100644
index 0000000..8c929b1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-controller.inc
@@ -0,0 +1,7 @@
+bind_socket = "*:11334";
+count = 1;
+secure_ip = "127.0.0.1";
+secure_ip = "::1";
+bind_socket = "/var/lib/rspamd/rspamd.sock mode=0666 owner=nobody";
+.include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc"
+.include(try=true; priority=30) "$CONFDIR/override.d/worker-controller.custom.inc"
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-fuzzy.inc b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-fuzzy.inc
new file mode 100644
index 0000000..291e615
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-fuzzy.inc
@@ -0,0 +1,12 @@
+# Socket to listen on (UDP and TCP from rspamd 1.3)
+bind_socket = "*:11445";
+allow_update = ["127.0.0.1", "::1"];
+# Number of processes to serve this storage (useful for read scaling)
+count = 1;
+# Backend ("sqlite" or "redis" - default "sqlite")
+backend = "redis";
+# Hashes storage time (3 months)
+expire = 90d;
+# Synchronize updates to the storage each minute
+sync = 1min;
+
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-normal.inc b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-normal.inc
new file mode 100644
index 0000000..c0f1fb1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-normal.inc
@@ -0,0 +1,4 @@
+bind_socket = "*:11333";
+task_timeout = 12s;
+count = 1;
+.include(try=true; priority=30) "$CONFDIR/override.d/worker-normal.custom.inc"
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-proxy.inc b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-proxy.inc
new file mode 100644
index 0000000..9eb4775
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/override.d/worker-proxy.inc
@@ -0,0 +1,9 @@
+bind_socket = "rspamd:9900";
+milter = true;
+upstream "local" {
+ name = "localhost";
+ default = true;
+ hosts = "rspamd:11333"
+}
+reject_message = "This message does not meet our delivery requirements";
+.include(try=true; priority=30) "$CONFDIR/override.d/worker-proxy.custom.inc"
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/plugins.d/README.md b/mailcow/src/mailcow-dockerized/data/conf/rspamd/plugins.d/README.md
new file mode 100644
index 0000000..1516cf2
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/plugins.d/README.md
@@ -0,0 +1 @@
+This is where you should copy any rspamd custom module
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/rspamd.conf.local b/mailcow/src/mailcow-dockerized/data/conf/rspamd/rspamd.conf.local
new file mode 100644
index 0000000..9f2f8f1
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/rspamd.conf.local
@@ -0,0 +1 @@
+# rspamd.conf.local
diff --git a/mailcow/src/mailcow-dockerized/data/conf/rspamd/rspamd.conf.override b/mailcow/src/mailcow-dockerized/data/conf/rspamd/rspamd.conf.override
new file mode 100644
index 0000000..d033e8e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/conf/rspamd/rspamd.conf.override
@@ -0,0 +1,2 @@
+# rspamd.conf.override
+