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
+