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/Dockerfiles/rspamd/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/Dockerfile
new file mode 100644
index 0000000..888bdcb
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/Dockerfile
@@ -0,0 +1,33 @@
+FROM debian:buster-slim
+LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
+
+ARG DEBIAN_FRONTEND=noninteractive
+ARG CODENAME=buster
+ENV LC_ALL C
+
+RUN apt-get update && apt-get install -y \
+  tzdata \
+  ca-certificates \
+  gnupg2 \
+  apt-transport-https \
+  dnsutils \
+  netcat \
+  && apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
+  && echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
+  && apt-get update \
+  && apt-get --no-install-recommends -y install rspamd redis-tools \
+  && rm -rf /var/lib/apt/lists/* \
+  && apt-get autoremove --purge \
+  && apt-get clean \
+  && mkdir -p /run/rspamd \
+  && chown _rspamd:_rspamd /run/rspamd
+
+COPY settings.conf /etc/rspamd/settings.conf
+COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
+COPY docker-entrypoint.sh /docker-entrypoint.sh
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+
+STOPSIGNAL SIGTERM
+
+CMD ["/usr/bin/rspamd", "-f", "-u", "_rspamd", "-g", "_rspamd"]
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/docker-entrypoint.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/docker-entrypoint.sh
new file mode 100755
index 0000000..203a196
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/docker-entrypoint.sh
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+until nc phpfpm 9001 -z; do
+  echo "Waiting for PHP on port 9001..."
+  sleep 3
+done
+
+until nc phpfpm 9002 -z; do
+  echo "Waiting for PHP on port 9002..."
+  sleep 3
+done
+
+mkdir -p /etc/rspamd/plugins.d \
+  /etc/rspamd/custom
+
+touch /etc/rspamd/rspamd.conf.local \
+  /etc/rspamd/rspamd.conf.override
+
+chmod 755 /var/lib/rspamd
+
+
+[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc
+
+echo ${IPV4_NETWORK}.0/24 > /etc/rspamd/custom/mailcow_networks.map
+echo ${IPV6_NETWORK} >> /etc/rspamd/custom/mailcow_networks.map
+
+DOVECOT_V4=
+DOVECOT_V6=
+until [[ ! -z ${DOVECOT_V4} ]]; do
+  DOVECOT_V4=$(dig a dovecot +short)
+  DOVECOT_V6=$(dig aaaa dovecot +short)
+  [[ ! -z ${DOVECOT_V4} ]] && break;
+  echo "Waiting for Dovecot..."
+  sleep 3
+done
+echo ${DOVECOT_V4}/32 > /etc/rspamd/custom/dovecot_trusted.map
+if [[ ! -z ${DOVECOT_V6} ]]; then
+  echo ${DOVECOT_V6}/128 >> /etc/rspamd/custom/dovecot_trusted.map
+fi
+
+RSPAMD_V4=
+RSPAMD_V6=
+until [[ ! -z ${RSPAMD_V4} ]]; do
+  RSPAMD_V4=$(dig a rspamd +short)
+  RSPAMD_V6=$(dig aaaa rspamd +short)
+  [[ ! -z ${RSPAMD_V4} ]] && break;
+  echo "Waiting for Rspamd..."
+  sleep 3
+done
+echo ${RSPAMD_V4}/32 > /etc/rspamd/custom/rspamd_trusted.map
+if [[ ! -z ${RSPAMD_V6} ]]; then
+  echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map
+fi
+
+if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
+  cat <<EOF > /etc/rspamd/local.d/redis.conf
+read_servers = "redis:6379";
+write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}";
+timeout = 10;
+EOF
+  until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do
+    echo "Waiting for Redis @redis-mailcow..."
+    sleep 2
+  done
+  until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} PING) == "PONG" ]]; do
+    echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..."
+    sleep 2
+  done
+  redis-cli -h redis-mailcow SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT}
+else
+  cat <<EOF > /etc/rspamd/local.d/redis.conf
+servers = "redis:6379";
+timeout = 10;
+EOF
+  until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do
+    echo "Waiting for Redis slave..."
+    sleep 2
+  done
+  redis-cli -h redis-mailcow SLAVEOF NO ONE
+fi
+
+chown -R _rspamd:_rspamd /var/lib/rspamd \
+  /etc/rspamd/local.d \
+  /etc/rspamd/override.d \
+  /etc/rspamd/rspamd.conf.local \
+  /etc/rspamd/rspamd.conf.override \
+  /etc/rspamd/plugins.d
+
+# Fix missing default global maps, if any
+# These exists in mailcow UI and should not be removed
+touch /etc/rspamd/custom/global_mime_from_blacklist.map \
+  /etc/rspamd/custom/global_rcpt_blacklist.map \
+  /etc/rspamd/custom/global_smtp_from_blacklist.map \
+  /etc/rspamd/custom/global_mime_from_whitelist.map \
+  /etc/rspamd/custom/global_rcpt_whitelist.map \
+  /etc/rspamd/custom/global_smtp_from_whitelist.map \
+  /etc/rspamd/custom/bad_languages.map \
+  /etc/rspamd/custom/sa-rules \
+  /etc/rspamd/custom/dovecot_trusted.map \
+  /etc/rspamd/custom/rspamd_trusted.map \
+  /etc/rspamd/custom/mailcow_networks.map \
+  /etc/rspamd/custom/ip_wl.map \
+  /etc/rspamd/custom/fishy_tlds.map \
+  /etc/rspamd/custom/bad_words.map \
+  /etc/rspamd/custom/bad_asn.map \
+  /etc/rspamd/custom/bad_words_de.map \
+  /etc/rspamd/custom/bulk_header.map
+
+# www-data (82) group needs to write to these files
+chown _rspamd:_rspamd /etc/rspamd/custom/
+chmod 0755 /etc/rspamd/custom/.
+chown -R 82:82 /etc/rspamd/custom/*
+chmod 644 -R /etc/rspamd/custom/*
+
+# Run hooks
+for file in /hooks/*; do
+  if [ -x "${file}" ]; then
+    echo "Running hook ${file}"
+    "${file}"
+  fi
+done
+
+exec "$@"
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/metadata_exporter.lua b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/metadata_exporter.lua
new file mode 100644
index 0000000..48a5ffc
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/metadata_exporter.lua
@@ -0,0 +1,632 @@
+--[[
+Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
+Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]--
+
+if confighelp then
+  return
+end
+
+-- A plugin that pushes metadata (or whole messages) to external services
+
+local redis_params
+local lua_util = require "lua_util"
+local rspamd_http = require "rspamd_http"
+local rspamd_util = require "rspamd_util"
+local rspamd_logger = require "rspamd_logger"
+local ucl = require "ucl"
+local E = {}
+local N = 'metadata_exporter'
+
+local settings = {
+  pusher_enabled = {},
+  pusher_format = {},
+  pusher_select = {},
+  mime_type = 'text/plain',
+  defer = false,
+  mail_from = '',
+  mail_to = 'postmaster@localhost',
+  helo = 'rspamd',
+  email_template = [[From: "Rspamd" <$mail_from>
+To: $mail_to
+Subject: Spam alert
+Date: $date
+MIME-Version: 1.0
+Message-ID: <$our_message_id>
+Content-type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+Authenticated username: $user
+IP: $ip
+Queue ID: $qid
+SMTP FROM: $from
+SMTP RCPT: $rcpt
+MIME From: $header_from
+MIME To: $header_to
+MIME Date: $header_date
+Subject: $header_subject
+Message-ID: $message_id
+Action: $action
+Score: $score
+Symbols: $symbols]],
+}
+
+local function get_general_metadata(task, flatten, no_content)
+  local r = {}
+  local ip = task:get_from_ip()
+  if ip and ip:is_valid() then
+    r.ip = tostring(ip)
+  else
+    r.ip = 'unknown'
+  end
+  r.user = task:get_user() or 'unknown'
+  r.qid = task:get_queue_id() or 'unknown'
+  r.subject = task:get_subject() or 'unknown'
+  r.action = task:get_metric_action('default')
+
+  local s = task:get_metric_score('default')[1]
+  r.score = flatten and string.format('%.2f', s) or s
+
+  local fuzzy = task:get_mempool():get_variable("fuzzy_hashes", "fstrings")
+  if fuzzy and #fuzzy > 0 then
+    local fz = {}
+    for _,h in ipairs(fuzzy) do
+      table.insert(fz, h)
+    end
+    if not flatten then
+      r.fuzzy = fz
+    else
+      r.fuzzy = table.concat(fz, ', ')
+    end
+  else
+    r.fuzzy = 'unknown'
+  end
+
+  local rcpt = task:get_recipients('smtp')
+  if rcpt then
+    local l = {}
+    for _, a in ipairs(rcpt) do
+      table.insert(l, a['addr'])
+    end
+    if not flatten then
+      r.rcpt = l
+    else
+      r.rcpt = table.concat(l, ', ')
+    end
+  else
+    r.rcpt = 'unknown'
+  end
+  local from = task:get_from('smtp')
+  if ((from or E)[1] or E).addr then
+    r.from = from[1].addr
+  else
+    r.from = 'unknown'
+  end
+  local syminf = task:get_symbols_all()
+  if flatten then
+    local l = {}
+    for _, sym in ipairs(syminf) do
+      local txt
+      if sym.options then
+        local topt = table.concat(sym.options, ', ')
+        txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')' .. ' [' .. topt .. ']'
+      else
+        txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')'
+      end
+      table.insert(l, txt)
+    end
+    r.symbols = table.concat(l, '\n\t')
+  else
+    r.symbols = syminf
+  end
+  local function process_header(name)
+    local hdr = task:get_header_full(name)
+    if hdr then
+      local l = {}
+      for _, h in ipairs(hdr) do
+        table.insert(l, h.decoded)
+      end
+      if not flatten then
+        return l
+      else
+        return table.concat(l, '\n')
+      end
+    else
+      return 'unknown'
+    end
+  end
+  if not no_content then
+    r.header_from = process_header('from')
+    r.header_to = process_header('to')
+    r.header_subject = process_header('subject')
+    r.header_date = process_header('date')
+    r.message_id = task:get_message_id()
+  end
+  return r
+end
+
+local formatters = {
+  default = function(task)
+    return task:get_content(), {}
+  end,
+  email_alert = function(task, rule, extra)
+    local meta = get_general_metadata(task, true)
+    local display_emails = {}
+    local mail_targets = {}
+    meta.mail_from = rule.mail_from or settings.mail_from
+    local mail_rcpt = rule.mail_to or settings.mail_to
+    if type(mail_rcpt) ~= 'table' then
+      table.insert(display_emails, string.format('<%s>', mail_rcpt))
+      table.insert(mail_targets, mail_rcpt)
+    else
+      for _, e in ipairs(mail_rcpt) do
+        table.insert(display_emails, string.format('<%s>', e))
+        table.insert(mail_targets, mail_rcpt)
+      end
+    end
+    if rule.email_alert_sender then
+      local x = task:get_from('smtp')
+      if x and string.len(x[1].addr) > 0 then
+        table.insert(mail_targets, x)
+        table.insert(display_emails, string.format('<%s>', x[1].addr))
+      end
+    end
+    if rule.email_alert_user then
+      local x = task:get_user()
+      if x then
+        table.insert(mail_targets, x)
+        table.insert(display_emails, string.format('<%s>', x))
+      end
+    end
+    if rule.email_alert_recipients then
+      local x = task:get_recipients('smtp')
+      if x then
+        for _, e in ipairs(x) do
+          if string.len(e.addr) > 0 then
+            table.insert(mail_targets, e.addr)
+            table.insert(display_emails, string.format('<%s>', e.addr))
+          end
+        end
+      end
+    end
+    meta.mail_to = table.concat(display_emails, ', ')
+    meta.our_message_id = rspamd_util.random_hex(12) .. '@rspamd'
+    meta.date = rspamd_util.time_to_string(rspamd_util.get_time())
+    return lua_util.template(rule.email_template or settings.email_template, meta), { mail_targets = mail_targets}
+  end,
+  json = function(task)
+    return ucl.to_format(get_general_metadata(task), 'json-compact')
+  end
+}
+
+local function is_spam(action)
+  return (action == 'reject' or action == 'add header' or action == 'rewrite subject')
+end
+
+local selectors = {
+  default = function(task)
+    return true
+  end,
+  is_spam = function(task)
+    local action = task:get_metric_action('default')
+    return is_spam(action)
+  end,
+  is_spam_authed = function(task)
+    if not task:get_user() then
+      return false
+    end
+    local action = task:get_metric_action('default')
+    return is_spam(action)
+  end,
+  is_reject = function(task)
+    local action = task:get_metric_action('default')
+    return (action == 'reject')
+  end,
+  is_reject_authed = function(task)
+    if not task:get_user() then
+      return false
+    end
+    local action = task:get_metric_action('default')
+    return (action == 'reject')
+  end,
+}
+
+local function maybe_defer(task, rule)
+  if rule.defer then
+    rspamd_logger.warnx(task, 'deferring message')
+    task:set_pre_result('soft reject', 'deferred', N)
+  end
+end
+
+local pushers = {
+  redis_pubsub = function(task, formatted, rule)
+    local _,ret,upstream
+    local function redis_pub_cb(err)
+      if err then
+        rspamd_logger.errx(task, 'got error %s when publishing on server %s',
+            err, upstream:get_addr())
+        return maybe_defer(task, rule)
+      end
+      return true
+    end
+    ret,_,upstream = rspamd_redis_make_request(task,
+      redis_params, -- connect params
+      nil, -- hash key
+      true, -- is write
+      redis_pub_cb, --callback
+      'PUBLISH', -- command
+      {rule.channel, formatted} -- arguments
+    )
+    if not ret then
+      rspamd_logger.errx(task, 'error connecting to redis')
+      maybe_defer(task, rule)
+    end
+  end,
+  http = function(task, formatted, rule)
+    local function http_callback(err, code)
+      if err then
+        rspamd_logger.errx(task, 'got error %s in http callback', err)
+        return maybe_defer(task, rule)
+      end
+      if code ~= 200 then
+        rspamd_logger.errx(task, 'got unexpected http status: %s', code)
+        return maybe_defer(task, rule)
+      end
+      return true
+    end
+    local hdrs = {}
+    if rule.meta_headers then
+      local gm = get_general_metadata(task, false, true)
+      local pfx = rule.meta_header_prefix or 'X-Rspamd-'
+      for k, v in pairs(gm) do
+        if type(v) == 'table' then
+          hdrs[pfx .. k] = ucl.to_format(v, 'json-compact')
+        else
+          hdrs[pfx .. k] = v
+        end
+      end
+    end
+    rspamd_http.request({
+      task=task,
+      url=rule.url,
+      body=formatted,
+      callback=http_callback,
+      mime_type=rule.mime_type or settings.mime_type,
+      headers=hdrs,
+    })
+  end,
+  send_mail = function(task, formatted, rule, extra)
+    local lua_smtp = require "lua_smtp"
+    local function sendmail_cb(ret, err)
+      if not ret then
+        rspamd_logger.errx(task, 'SMTP export error: %s', err)
+        maybe_defer(task, rule)
+      end
+    end
+
+    lua_smtp.sendmail({
+      task = task,
+      host = rule.smtp,
+      port = rule.smtp_port or settings.smtp_port or 25,
+      from = rule.mail_from or settings.mail_from,
+      recipients = extra.mail_targets or rule.mail_to or settings.mail_to,
+      helo = rule.helo or settings.helo,
+      timeout = rule.timeout or settings.timeout,
+    }, formatted, sendmail_cb)
+  end,
+}
+
+local opts = rspamd_config:get_all_opt(N)
+if not opts then return end
+local process_settings = {
+  select = function(val)
+    selectors.custom = assert(load(val))()
+  end,
+  format = function(val)
+    formatters.custom = assert(load(val))()
+  end,
+  push = function(val)
+    pushers.custom = assert(load(val))()
+  end,
+  custom_push = function(val)
+    if type(val) == 'table' then
+      for k, v in pairs(val) do
+        pushers[k] = assert(load(v))()
+      end
+    end
+  end,
+  custom_select = function(val)
+    if type(val) == 'table' then
+      for k, v in pairs(val) do
+        selectors[k] = assert(load(v))()
+      end
+    end
+  end,
+  custom_format = function(val)
+    if type(val) == 'table' then
+      for k, v in pairs(val) do
+        formatters[k] = assert(load(v))()
+      end
+    end
+  end,
+  pusher_enabled = function(val)
+    if type(val) == 'string' then
+      if pushers[val] then
+        settings.pusher_enabled[val] = true
+      else
+        rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
+      end
+    elseif type(val) == 'table' then
+      for _, v in ipairs(val) do
+        if pushers[v] then
+          settings.pusher_enabled[v] = true
+        else
+          rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
+        end
+      end
+    end
+  end,
+}
+for k, v in pairs(opts) do
+  local f = process_settings[k]
+  if f then
+    f(opts[k])
+  else
+    settings[k] = v
+  end
+end
+if type(settings.rules) ~= 'table' then
+  -- Legacy config
+  settings.rules = {}
+  if not next(settings.pusher_enabled) then
+    if pushers.custom then
+      rspamd_logger.infox(rspamd_config, 'Custom pusher implicitly enabled')
+      settings.pusher_enabled.custom = true
+    else
+      -- Check legacy options
+      if settings.url then
+        rspamd_logger.warnx(rspamd_config, 'HTTP pusher implicitly enabled')
+        settings.pusher_enabled.http = true
+      end
+      if settings.channel then
+        rspamd_logger.warnx(rspamd_config, 'Redis Pubsub pusher implicitly enabled')
+        settings.pusher_enabled.redis_pubsub = true
+      end
+      if settings.smtp and settings.mail_to then
+        rspamd_logger.warnx(rspamd_config, 'SMTP pusher implicitly enabled')
+        settings.pusher_enabled.send_mail = true
+      end
+    end
+  end
+  if not next(settings.pusher_enabled) then
+    rspamd_logger.errx(rspamd_config, 'No push backend enabled')
+    return
+  end
+  if settings.formatter then
+    settings.format = formatters[settings.formatter]
+    if not settings.format then
+      rspamd_logger.errx(rspamd_config, 'No such formatter: %s', settings.formatter)
+      return
+    end
+  end
+  if settings.selector then
+    settings.select = selectors[settings.selector]
+    if not settings.select then
+      rspamd_logger.errx(rspamd_config, 'No such selector: %s', settings.selector)
+      return
+    end
+  end
+  for k in pairs(settings.pusher_enabled) do
+    local formatter = settings.pusher_format[k]
+    local selector = settings.pusher_select[k]
+    if not formatter then
+      settings.pusher_format[k] = settings.formatter or 'default'
+      rspamd_logger.infox(rspamd_config, 'Using default formatter for %s pusher', k)
+    else
+      if not formatters[formatter] then
+        rspamd_logger.errx(rspamd_config, 'No such formatter: %s - disabling %s', formatter, k)
+        settings.pusher_enabled.k = nil
+      end
+    end
+    if not selector then
+      settings.pusher_select[k] = settings.selector or 'default'
+      rspamd_logger.infox(rspamd_config, 'Using default selector for %s pusher', k)
+    else
+      if not selectors[selector] then
+        rspamd_logger.errx(rspamd_config, 'No such selector: %s - disabling %s', selector, k)
+        settings.pusher_enabled.k = nil
+      end
+    end
+  end
+  if settings.pusher_enabled.redis_pubsub then
+    redis_params = rspamd_parse_redis_server(N)
+    if not redis_params then
+      rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
+      settings.pusher_enabled.redis_pubsub = nil
+    else
+      local r = {}
+      r.backend = 'redis_pubsub'
+      r.channel = settings.channel
+      r.defer = settings.defer
+      r.selector = settings.pusher_select.redis_pubsub
+      r.formatter = settings.pusher_format.redis_pubsub
+      settings.rules[r.backend:upper()] = r
+    end
+  end
+  if settings.pusher_enabled.http then
+    if not settings.url then
+      rspamd_logger.errx(rspamd_config, 'No URL is specified')
+      settings.pusher_enabled.http = nil
+    else
+      local r = {}
+      r.backend = 'http'
+      r.url = settings.url
+      r.mime_type = settings.mime_type
+      r.defer = settings.defer
+      r.selector = settings.pusher_select.http
+      r.formatter = settings.pusher_format.http
+      settings.rules[r.backend:upper()] = r
+    end
+  end
+  if settings.pusher_enabled.send_mail then
+    if not (settings.mail_to and settings.smtp) then
+      rspamd_logger.errx(rspamd_config, 'No mail_to and/or smtp setting is specified')
+      settings.pusher_enabled.send_mail = nil
+    else
+      local r = {}
+      r.backend = 'send_mail'
+      r.mail_to = settings.mail_to
+      r.mail_from = settings.mail_from
+      r.helo = settings.hello
+      r.smtp = settings.smtp
+      r.smtp_port = settings.smtp_port
+      r.email_template = settings.email_template
+      r.defer = settings.defer
+      r.selector = settings.pusher_select.send_mail
+      r.formatter = settings.pusher_format.send_mail
+      settings.rules[r.backend:upper()] = r
+    end
+  end
+  if not next(settings.pusher_enabled) then
+    rspamd_logger.errx(rspamd_config, 'No push backend enabled')
+    return
+  end
+elseif not next(settings.rules) then
+  lua_util.debugm(N, rspamd_config, 'No rules enabled')
+  return
+end
+if not settings.rules or not next(settings.rules) then
+  rspamd_logger.errx(rspamd_config, 'No rules enabled')
+  return
+end
+local backend_required_elements = {
+  http = {
+    'url',
+  },
+  smtp = {
+    'mail_to',
+    'smtp',
+  },
+  redis_pubsub = {
+    'channel',
+  },
+}
+local check_element = {
+  selector = function(k, v)
+    if not selectors[v] then
+      rspamd_logger.errx(rspamd_config, 'Rule %s has invalid selector %s', k, v)
+      return false
+    else
+      return true
+    end
+  end,
+  formatter = function(k, v)
+    if not formatters[v] then
+      rspamd_logger.errx(rspamd_config, 'Rule %s has invalid formatter %s', k, v)
+      return false
+    else
+      return true
+    end
+  end,
+}
+local backend_check = {
+  default = function(k, rule)
+    local reqset = backend_required_elements[rule.backend]
+    if reqset then
+      for _, e in ipairs(reqset) do
+        if not rule[e] then
+          rspamd_logger.errx(rspamd_config, 'Rule %s misses required setting %s', k, e)
+          settings.rules[k] = nil
+        end
+      end
+    end
+    for sett, v in pairs(rule) do
+      local f = check_element[sett]
+      if f then
+        if not f(sett, v) then
+          settings.rules[k] = nil
+        end
+      end
+    end
+  end,
+}
+backend_check.redis_pubsub = function(k, rule)
+  if not redis_params then
+    redis_params = rspamd_parse_redis_server(N)
+  end
+  if not redis_params then
+    rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
+    settings.rules[k] = nil
+  else
+    backend_check.default(k, rule)
+  end
+end
+setmetatable(backend_check, {
+  __index = function()
+    return backend_check.default
+  end,
+})
+for k, v in pairs(settings.rules) do
+  if type(v) == 'table' then
+    local backend = v.backend
+    if not backend then
+      rspamd_logger.errx(rspamd_config, 'Rule %s has no backend', k)
+      settings.rules[k] = nil
+    elseif not pushers[backend] then
+      rspamd_logger.errx(rspamd_config, 'Rule %s has invalid backend %s', k, backend)
+      settings.rules[k] = nil
+    else
+      local f = backend_check[backend]
+      f(k, v)
+    end
+  else
+    rspamd_logger.errx(rspamd_config, 'Rule %s has bad type: %s', k, type(v))
+    settings.rules[k] = nil
+  end
+end
+
+local function gen_exporter(rule)
+  return function (task)
+    if task:has_flag('skip') then return end
+    local selector = rule.selector or 'default'
+    local selected = selectors[selector](task)
+    if selected then
+      lua_util.debugm(N, task, 'Message selected for processing')
+      local formatter = rule.formatter or 'default'
+      local formatted, extra = formatters[formatter](task, rule)
+      if formatted then
+        pushers[rule.backend](task, formatted, rule, extra)
+      else
+        lua_util.debugm(N, task, 'Formatter [%s] returned non-truthy value [%s]', formatter, formatted)
+      end
+    else
+      lua_util.debugm(N, task, 'Selector [%s] returned non-truthy value [%s]', selector, selected)
+    end
+  end
+end
+
+if not next(settings.rules) then
+  rspamd_logger.errx(rspamd_config, 'No rules enabled')
+  lua_util.disable_module(N, "config")
+end
+for k, r in pairs(settings.rules) do
+  rspamd_config:register_symbol({
+    name = 'EXPORT_METADATA_' .. k,
+    type = 'idempotent',
+    callback = gen_exporter(r),
+    priority = 10,
+    flags = 'empty,explicit_disable,ignore_passthrough',
+  })
+end
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/sa_trivial_convert.lua b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/sa_trivial_convert.lua
new file mode 100644
index 0000000..4725dab
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/sa_trivial_convert.lua
@@ -0,0 +1,443 @@
+local fun = require "fun"
+local rspamd_logger = require "rspamd_logger"
+local util = require "rspamd_util"
+local lua_util = require "lua_util"
+local rspamd_regexp = require "rspamd_regexp"
+local ucl = require "ucl"
+
+local complicated = {}
+local rules = {}
+local scores = {}
+
+local function words_to_re(words, start)
+  return table.concat(fun.totable(fun.drop_n(start, words)), " ");
+end
+
+local function split(str, delim)
+  local result = {}
+
+  if not delim then
+    delim = '[^%s]+'
+  end
+
+  for token in string.gmatch(str, delim) do
+    table.insert(result, token)
+  end
+
+  return result
+end
+
+local function handle_header_def(hline, cur_rule)
+  --Now check for modifiers inside header's name
+  local hdrs = split(hline, '[^|]+')
+  local hdr_params = {}
+  local cur_param = {}
+  -- Check if an re is an ordinary re
+  local ordinary = true
+
+  for _,h in ipairs(hdrs) do
+    if h == 'ALL' or h == 'ALL:raw' then
+      ordinary = false
+    else
+      local args = split(h, '[^:]+')
+      cur_param['strong'] = false
+      cur_param['raw'] = false
+      cur_param['header'] = args[1]
+
+      if args[2] then
+        -- We have some ops that are required for the header, so it's not ordinary
+        ordinary = false
+      end
+
+      fun.each(function(func)
+          if func == 'addr' then
+            cur_param['function'] = function(str)
+              local addr_parsed = util.parse_addr(str)
+              local ret = {}
+              if addr_parsed then
+                for _,elt in ipairs(addr_parsed) do
+                  if elt['addr'] then
+                    table.insert(ret, elt['addr'])
+                  end
+                end
+              end
+
+              return ret
+            end
+          elseif func == 'name' then
+            cur_param['function'] = function(str)
+              local addr_parsed = util.parse_addr(str)
+              local ret = {}
+              if addr_parsed then
+                for _,elt in ipairs(addr_parsed) do
+                  if elt['name'] then
+                    table.insert(ret, elt['name'])
+                  end
+                end
+              end
+
+              return ret
+            end
+          elseif func == 'raw' then
+            cur_param['raw'] = true
+          elseif func == 'case' then
+            cur_param['strong'] = true
+          else
+            rspamd_logger.warnx(rspamd_config, 'Function %1 is not supported in %2',
+              func, cur_rule['symbol'])
+          end
+        end, fun.tail(args))
+
+        -- Some header rules require splitting to check of multiple headers
+        if cur_param['header'] == 'MESSAGEID' then
+          -- Special case for spamassassin
+          ordinary = false
+        elseif cur_param['header'] == 'ToCc' then
+          ordinary = false
+        else
+          table.insert(hdr_params, cur_param)
+        end
+    end
+
+    cur_rule['ordinary'] = ordinary and (not (#hdr_params > 1))
+    cur_rule['header'] = hdr_params
+  end
+end
+
+local function process_sa_conf(f)
+  local cur_rule = {}
+  local valid_rule = false
+
+  local function insert_cur_rule()
+   if not rules[cur_rule.type] then
+     rules[cur_rule.type] = {}
+   end
+
+   local target = rules[cur_rule.type]
+
+   if cur_rule.type == 'header' then
+     if not cur_rule.header[1].header then
+      rspamd_logger.errx(rspamd_config, 'bad rule definition: %1', cur_rule)
+      return
+     end
+     if not target[cur_rule.header[1].header] then
+       target[cur_rule.header[1].header] = {}
+     end
+     target = target[cur_rule.header[1].header]
+   end
+
+   if not cur_rule['symbol'] then
+     rspamd_logger.errx(rspamd_config, 'bad rule definition: %1', cur_rule)
+     return
+   end
+   target[cur_rule['symbol']] = cur_rule
+   cur_rule = {}
+   valid_rule = false
+  end
+
+  local function parse_score(words)
+    if #words == 3 then
+      -- score rule <x>
+      return tonumber(words[3])
+    elseif #words == 6 then
+      -- score rule <x1> <x2> <x3> <x4>
+      -- we assume here that bayes and network are enabled and select <x4>
+      return tonumber(words[6])
+    else
+      rspamd_logger.errx(rspamd_config, 'invalid score for %1', words[2])
+    end
+
+    return 0
+  end
+
+  local skip_to_endif = false
+  local if_nested = 0
+  for l in f:lines() do
+    (function ()
+    l = lua_util.rspamd_str_trim(l)
+    -- Replace bla=~/re/ with bla =~ /re/ (#2372)
+    l = l:gsub('([^%s])%s*([=!]~)%s*([^%s])', '%1 %2 %3')
+
+    if string.len(l) == 0 or string.sub(l, 1, 1) == '#' then
+      return
+    end
+
+    -- Unbalanced if/endif
+    if if_nested < 0 then if_nested = 0 end
+    if skip_to_endif then
+      if string.match(l, '^endif') then
+        if_nested = if_nested - 1
+
+        if if_nested == 0 then
+          skip_to_endif = false
+        end
+      elseif string.match(l, '^if') then
+        if_nested = if_nested + 1
+      elseif string.match(l, '^else') then
+        -- Else counterpart for if
+        skip_to_endif = false
+      end
+      table.insert(complicated, l)
+      return
+    else
+      if string.match(l, '^ifplugin') then
+        skip_to_endif = true
+        if_nested = if_nested + 1
+        table.insert(complicated, l)
+      elseif string.match(l, '^if !plugin%(') then
+         skip_to_endif = true
+         if_nested = if_nested + 1
+        table.insert(complicated, l)
+      elseif string.match(l, '^if') then
+        -- Unknown if
+        skip_to_endif = true
+        if_nested = if_nested + 1
+        table.insert(complicated, l)
+      elseif string.match(l, '^else') then
+        -- Else counterpart for if
+        skip_to_endif = true
+        table.insert(complicated, l)
+      elseif string.match(l, '^endif') then
+        if_nested = if_nested - 1
+        table.insert(complicated, l)
+      end
+    end
+
+    -- Skip comments
+    local words = fun.totable(fun.take_while(
+      function(w) return string.sub(w, 1, 1) ~= '#' end,
+      fun.filter(function(w)
+          return w ~= "" end,
+      fun.iter(split(l)))))
+
+    if words[1] == "header" then
+      -- header SYMBOL Header ~= /regexp/
+      if valid_rule then
+        insert_cur_rule()
+      end
+      if words[4] and (words[4] == '=~' or words[4] == '!~') then
+        cur_rule['type'] = 'header'
+        cur_rule['symbol'] = words[2]
+
+        if words[4] == '!~' then
+          table.insert(complicated, l)
+          return
+        end
+
+        cur_rule['re_expr'] = words_to_re(words, 4)
+        local unset_comp = string.find(cur_rule['re_expr'], '%s+%[if%-unset:')
+        if unset_comp then
+          table.insert(complicated, l)
+          return
+        end
+
+        cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr'])
+
+        if not cur_rule['re'] then
+          rspamd_logger.warnx(rspamd_config, "Cannot parse regexp '%1' for %2",
+            cur_rule['re_expr'], cur_rule['symbol'])
+          table.insert(complicated, l)
+          return
+        else
+          handle_header_def(words[3], cur_rule)
+          if not cur_rule['ordinary'] then
+            table.insert(complicated, l)
+            return
+          end
+        end
+
+        valid_rule = true
+      else
+        table.insert(complicated, l)
+        return
+      end
+    elseif words[1] == "body" then
+      -- body SYMBOL /regexp/
+      if valid_rule then
+        insert_cur_rule()
+      end
+
+      cur_rule['symbol'] = words[2]
+      if words[3] and (string.sub(words[3], 1, 1) == '/'
+          or string.sub(words[3], 1, 1) == 'm') then
+        cur_rule['type'] = 'sabody'
+        cur_rule['re_expr'] = words_to_re(words, 2)
+        cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr'])
+        if cur_rule['re'] then
+
+          valid_rule = true
+        end
+      else
+        -- might be function
+        table.insert(complicated, l)
+        return
+      end
+    elseif words[1] == "rawbody" then
+      -- body SYMBOL /regexp/
+      if valid_rule then
+        insert_cur_rule()
+      end
+
+      cur_rule['symbol'] = words[2]
+      if words[3] and (string.sub(words[3], 1, 1) == '/'
+          or string.sub(words[3], 1, 1) == 'm') then
+        cur_rule['type'] = 'sarawbody'
+        cur_rule['re_expr'] = words_to_re(words, 2)
+        cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr'])
+        if cur_rule['re'] then
+          valid_rule = true
+        end
+      else
+        table.insert(complicated, l)
+        return
+      end
+    elseif words[1] == "full" then
+      -- body SYMBOL /regexp/
+      if valid_rule then
+        insert_cur_rule()
+      end
+
+      cur_rule['symbol'] = words[2]
+
+      if words[3] and (string.sub(words[3], 1, 1) == '/'
+          or string.sub(words[3], 1, 1) == 'm') then
+        cur_rule['type'] = 'message'
+        cur_rule['re_expr'] = words_to_re(words, 2)
+        cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr'])
+        cur_rule['raw'] = true
+        if cur_rule['re'] then
+          valid_rule = true
+        end
+      else
+        table.insert(complicated, l)
+        return
+      end
+    elseif words[1] == "uri" then
+      -- uri SYMBOL /regexp/
+      if valid_rule then
+        insert_cur_rule()
+      end
+      cur_rule['type'] = 'uri'
+      cur_rule['symbol'] = words[2]
+      cur_rule['re_expr'] = words_to_re(words, 2)
+      cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr'])
+      if cur_rule['re'] and cur_rule['symbol'] then
+        valid_rule = true
+      else
+        table.insert(complicated, l)
+        return
+      end
+    elseif words[1] == "meta" then
+      -- meta SYMBOL expression
+      if valid_rule then
+        insert_cur_rule()
+      end
+      table.insert(complicated, l)
+      return
+    elseif words[1] == "describe" and valid_rule then
+      cur_rule['description'] = words_to_re(words, 2)
+    elseif words[1] == "score" then
+      scores[words[2]] = parse_score(words)
+    else
+      table.insert(complicated, l)
+      return
+    end
+    end)()
+  end
+  if valid_rule then
+    insert_cur_rule()
+  end
+end
+
+for _,matched in ipairs(arg) do
+  local f = io.open(matched, "r")
+  if f then
+    rspamd_logger.messagex(rspamd_config, 'loading SA rules from %s', matched)
+    process_sa_conf(f)
+  else
+    rspamd_logger.errx(rspamd_config, "cannot open %1", matched)
+  end
+end
+
+local multimap_conf = {}
+
+local function handle_rule(what, syms, hdr)
+  local mtype
+  local filter
+  local fname
+  local header
+  local sym = what:upper()
+  if what == 'sabody' then
+    mtype = 'content'
+    fname = 'body_re.map'
+    filter = 'oneline'
+  elseif what == 'sarawbody' then
+    fname = 'raw_body_re.map'
+    mtype = 'content'
+    filter = 'rawtext'
+  elseif what == 'full' then
+    fname = 'full_re.map'
+    mtype = 'content'
+    filter = 'full'
+  elseif what == 'uri' then
+    fname = 'uri_re.map'
+    mtype = 'url'
+    filter = 'full'
+  elseif what == 'header' then
+    fname = ('hdr_' .. hdr .. '_re.map'):lower()
+    mtype = 'header'
+    header = hdr
+    sym = sym .. '_' .. hdr:upper()
+  else
+    rspamd_logger.errx('unknown type: %s', what)
+    return
+  end
+  local conf = {
+    type = mtype,
+    filter = filter,
+    symbol = 'SA_MAP_AUTO_' .. sym,
+    regexp = true,
+    map = fname,
+    header = header,
+    symbols = {}
+  }
+  local re_file = io.open(fname, 'w')
+
+  for k,r in pairs(syms) do
+    local score = 0.0
+    if scores[k] then
+      score = scores[k]
+    end
+    re_file:write(string.format('/%s/ %s:%f\n', tostring(r.re), k, score))
+    table.insert(conf.symbols, k)
+  end
+
+  re_file:close()
+
+  multimap_conf[sym:lower()] = conf
+  rspamd_logger.messagex('stored %s regexp in %s', sym:lower(), fname)
+end
+
+for k,v in pairs(rules) do
+  if k == 'header' then
+    for h,r in pairs(v) do
+      handle_rule(k, r, h)
+    end
+  else
+    handle_rule(k, v)
+  end
+end
+
+local out = ucl.to_format(multimap_conf, 'ucl')
+local mmap_conf = io.open('auto_multimap.conf', 'w')
+mmap_conf:write(out)
+mmap_conf:close()
+rspamd_logger.messagex('stored multimap conf in %s', 'auto_multimap.conf')
+
+local sa_remain = io.open('auto_sa.conf', 'w')
+fun.each(function(l) 
+  sa_remain:write(l)
+  sa_remain:write('\n')
+end, fun.filter(function(l) return not string.match(l, '^%s+$') end, complicated))
+sa_remain:close()
+rspamd_logger.messagex('stored sa remains conf in %s', 'auto_sa.conf')
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/settings.conf b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/settings.conf
new file mode 100644
index 0000000..4449f09
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/settings.conf
@@ -0,0 +1 @@
+settings = "http://nginx:8081/settings.php";