git subrepo commit (merge) mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "32243e56"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "e2b4b6f6"
git-subrepo: version:  "0.4.3"
  origin:   "???"
  commit:   "???"
Change-Id: I51e2016ef5ab88a8b0bdc08551b18f48ceef0aa5
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/Dockerfile
index a19c434..82369a8 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.13
+FROM alpine:3.14
 
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile
index b251d96..e14858f 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile
@@ -2,8 +2,7 @@
 
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 
-ARG CLAMAV=0.103.2
-
+ARG CLAMAV=0.103.4
 RUN apt-get update && apt-get install -y --no-install-recommends \
   ca-certificates \
   zlib1g-dev \
@@ -24,7 +23,7 @@
   dos2unix \
   netcat \
   && rm -rf /var/lib/apt/lists/* \
-  && wget -O - https://www.clamav.net/downloads/production/clamav-${CLAMAV}.tar.gz | tar xfvz - \
+  && wget -O - https://fossies.org/linux/misc/clamav-${CLAMAV}.tar.gz | tar xfvz - \
   && cd clamav-${CLAMAV} \
   && ./configure \
   --prefix=/usr \
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/Dockerfile
index 645503a..b6eef46 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.13
+FROM alpine:3.14
 
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile
index 1990097..1fc4000 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile
@@ -2,7 +2,7 @@
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG DOVECOT=2.3.14.1
+ARG DOVECOT=2.3.17
 ENV LC_ALL C
 ENV GOSU_VERSION 1.12
 
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/docker-entrypoint.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/docker-entrypoint.sh
index 5ea1609..9ac2dc6 100755
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/docker-entrypoint.sh
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/docker-entrypoint.sh
@@ -155,34 +155,47 @@
   local row = cur:fetch ({}, "a")
   while row do
     if req.password_verify(req, row.password, pass) == 1 then
-      cur:close()
       con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
         VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
+      cur:close()
+      con:close()
       return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
     end
     row = cur:fetch (row, "a")
   end
 
-  -- check against app passwds
-  local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, app_passwd.password FROM app_passwd
-    INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
-    WHERE mailbox = '%s'
-      AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.%s_access')), 1) = '1'
-      AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1'
-      AND app_passwd.active = '1'
-      AND mailbox.active = '1'
-      AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.service), con:escape(req.domain)))
-  local row = cur:fetch ({}, "a")
-  while row do
-    if req.password_verify(req, row.password, pass) == 1 then
-      cur:close()
-      con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
-        VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
-      return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
+  -- check against app passwds for imap and smtp
+  -- app passwords are only available for imap, smtp, sieve and pop3 when using sasl
+  if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then
+    local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd
+      INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
+      WHERE mailbox = '%s'
+        AND app_passwd.active = '1'
+        AND mailbox.active = '1'
+        AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain)))
+    local row = cur:fetch ({}, "a")
+    while row do
+      if req.password_verify(req, row.password, pass) == 1 then
+        -- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed
+        if tostring(req.real_rip) == "__IPV4_SOGO__" then
+          cur:close()
+          con:close()
+          return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
+        elseif row.has_prot_access == "1" then
+          con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
+            VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
+          cur:close()
+          con:close()
+          return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
+        end
+      end
+      row = cur:fetch (row, "a")
     end
-    row = cur:fetch (row, "a")
   end
 
+  cur:close()
+  con:close()
+
   return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
 
   -- PoC
@@ -230,6 +243,7 @@
 sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
 sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
 sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
+sed -i "s/__IPV4_SOGO__/${IPV4_NETWORK}.248/g" /etc/dovecot/lua/passwd-verify.lua
 
 
 # Migrate old sieve_after file
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/imapsync_runner.pl b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/imapsync_runner.pl
index 0f01a97..5b297ab 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/imapsync_runner.pl
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/imapsync_runner.pl
@@ -152,18 +152,28 @@
   '--noreleasecheck'];
 
   try {
-    $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?");
+    $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?");
     $is_running->bind_param( 1, ${id} );
     $is_running->execute();
-    
+
     run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout;
-    
-    $update = $dbh->prepare("UPDATE imapsync SET returned_text = ? WHERE id = ?");
+
+    # check exit code and status
+    ($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/);
+
+    $success = 0;
+    if (defined $exit_code && $exit_code == 0) {
+      $success = 1;
+    }
+
+    $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?");
     $update->bind_param( 1, ${stdout} );
-    $update->bind_param( 2, ${id} );
+    $update->bind_param( 2, ${success} );
+    $update->bind_param( 3, ${exit_status} );
+    $update->bind_param( 4, ${id} );
     $update->execute();
   } catch {
-    $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync' WHERE id = ?");
+    $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?");
     $update->bind_param( 1, ${id} );
     $update->execute();
   } finally {
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile
index 007bd64..c63e99b 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.13
+FROM alpine:3.14
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ENV XTABLES_LIBDIR /usr/lib/xtables
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
index 04f6c47..08c8727 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
@@ -92,15 +92,16 @@
   global exit_code

   if not r.get('F2B_REGEX'):

     f2bregex = {}

-    f2bregex[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'

-    f2bregex[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'

-    f2bregex[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'

-    f2bregex[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'

-    f2bregex[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'

-    f2bregex[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'

-    f2bregex[7] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'

-    f2bregex[8] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'

-    f2bregex[9] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'

+    f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'

+    f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'

+    f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'

+    f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'

+    f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'

+    f2bregex[6] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'

+    f2bregex[7] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'

+    f2bregex[8] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'

+    f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'

+    f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'

     r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))

   else:

     try:

@@ -122,8 +123,10 @@
     time.sleep(10)

     with lock:

       filter4_table = iptc.Table(iptc.Table.FILTER)

+      filter6_table = iptc.Table6(iptc.Table6.FILTER)

       filter4_table.refresh()

-      for f in [filter4_table]:

+      filter6_table.refresh()

+      for f in [filter4_table, filter6_table]:

         forward_chain = iptc.Chain(f, 'FORWARD')

         input_chain = iptc.Chain(f, 'INPUT')

         for chain in [forward_chain, input_chain]:

@@ -195,7 +198,14 @@
         if rule not in chain.rules:

           chain.insert_rule(rule)

     else:

-      pass

+      with lock:

+        chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')

+        rule = iptc.Rule6()

+        rule.src = net

+        target = iptc.Target(rule, "REJECT")

+        rule.target = target

+        if rule not in chain.rules:

+          chain.insert_rule(rule)

     r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)

   else:

     logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))

@@ -217,7 +227,14 @@
       if rule in chain.rules:

         chain.delete_rule(rule)

   else:

-    pass

+    with lock:

+      chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')

+      rule = iptc.Rule6()

+      rule.src = net

+      target = iptc.Target(rule, "REJECT")

+      rule.target = target

+      if rule in chain.rules:

+        chain.delete_rule(rule)

   r.hdel('F2B_ACTIVE_BANS', '%s' % net)

   r.hdel('F2B_QUEUE_UNBAN', '%s' % net)

   if net in bans:

@@ -235,13 +252,26 @@
       if rule not in chain.rules and not unban:

         logCrit('Add host/network %s to blacklist' % net)

         chain.insert_rule(rule)

-        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))

+        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) 

       elif rule in chain.rules and unban:

         logCrit('Remove host/network %s from blacklist' % net)

         chain.delete_rule(rule)

         r.hdel('F2B_PERM_BANS', '%s' % net)

   else:

-    pass

+    with lock:

+      chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')

+      rule = iptc.Rule6()

+      rule.src = net

+      target = iptc.Target(rule, "REJECT")

+      rule.target = target

+      if rule not in chain.rules and not unban:

+        logCrit('Add host/network %s to blacklist' % net)

+        chain.insert_rule(rule)

+        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) 

+      elif rule in chain.rules and unban:

+        logCrit('Remove host/network %s from blacklist' % net)

+        chain.delete_rule(rule)

+        r.hdel('F2B_PERM_BANS', '%s' % net)

 

 def quit(signum, frame):

   global quit_now

@@ -254,7 +284,8 @@
     unban(net)

   with lock:

     filter4_table = iptc.Table(iptc.Table.FILTER)

-    for filter_table in [filter4_table]:

+    filter6_table = iptc.Table6(iptc.Table6.FILTER)

+    for filter_table in [filter4_table, filter6_table]:

       filter_table.autocommit = False

       forward_chain = iptc.Chain(filter_table, "FORWARD")

       input_chain = iptc.Chain(filter_table, "INPUT")

@@ -337,7 +368,41 @@
           table.commit()

         table.autocommit = True

       except:

-        print('Error running SNAT4, retrying...')

+        print('Error running SNAT4, retrying...') 

+

+def snat6(snat_target):

+  global lock

+  global quit_now

+

+  def get_snat6_rule():

+    rule = iptc.Rule6()

+    rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')

+    rule.dst = '!' + rule.src

+    target = rule.create_target("SNAT")

+    target.to_source = snat_target

+    return rule

+

+  while not quit_now:

+    time.sleep(10)

+    with lock:

+      try:

+        table = iptc.Table6('nat')

+        table.refresh()

+        chain = iptc.Chain(table, 'POSTROUTING')

+        table.autocommit = False

+        if get_snat6_rule() not in chain.rules:

+          logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))

+          chain.insert_rule(get_snat6_rule())

+          table.commit()

+        else:

+          for position, item in enumerate(chain.rules):

+            if item == get_snat6_rule():

+              if position != 0:

+                chain.delete_rule(get_snat6_rule())

+          table.commit()

+        table.autocommit = True

+      except:

+        print('Error running SNAT6, retrying...') 

 

 def autopurge():

   while not quit_now:

@@ -403,7 +468,7 @@
       if Counter(new_whitelist) != Counter(WHITELIST):

         WHITELIST = new_whitelist

         logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))

-    time.sleep(60.0 - ((time.time() - start_time) % 60.0))

+    time.sleep(60.0 - ((time.time() - start_time) % 60.0)) 

 

 def blacklistUpdate():

   global quit_now

@@ -414,7 +479,7 @@
     new_blacklist = []

     if list:

       new_blacklist = genNetworkList(list)

-    if Counter(new_blacklist) != Counter(BLACKLIST):

+    if Counter(new_blacklist) != Counter(BLACKLIST): 

       addban = set(new_blacklist).difference(BLACKLIST)

       delban = set(BLACKLIST).difference(new_blacklist)

       BLACKLIST = new_blacklist

@@ -425,7 +490,7 @@
       if delban:

         for net in delban:

           permBan(net=net, unban=True)

-    time.sleep(60.0 - ((time.time() - start_time) % 60.0))

+    time.sleep(60.0 - ((time.time() - start_time) % 60.0)) 

 

 def initChain():

   # Is called before threads start, no locking

@@ -442,6 +507,18 @@
     rule.target = target

     if rule not in chain.rules:

       chain.insert_rule(rule)

+  # IPv6

+  if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:

+    iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")

+  for c in ['FORWARD', 'INPUT']:

+    chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)

+    rule = iptc.Rule6()

+    rule.src = '::/0'

+    rule.dst = '::/0'

+    target = iptc.Target(rule, "MAILCOW")

+    rule.target = target

+    if rule not in chain.rules:

+      chain.insert_rule(rule)

 

 if __name__ == '__main__':

 

@@ -465,6 +542,17 @@
     except ValueError:

       print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')

 

+  if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':

+    try:

+      snat_ip = os.getenv('SNAT6_TO_SOURCE')

+      snat_ipo = ipaddress.ip_address(snat_ip)

+      if type(snat_ipo) is ipaddress.IPv6Address:

+        snat6_thread = Thread(target=snat6,args=(snat_ip,))

+        snat6_thread.daemon = True

+        snat6_thread.start()

+    except ValueError:

+      print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')

+

   autopurge_thread = Thread(target=autopurge)

   autopurge_thread.daemon = True

   autopurge_thread.start()

diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/olefy/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/olefy/Dockerfile
index 3cacabb..6d9727c 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/olefy/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/olefy/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.13
+FROM alpine:3.14
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 WORKDIR /app
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/phpfpm/docker-entrypoint.sh
index 9a2b582..cefebcd 100755
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/phpfpm/docker-entrypoint.sh
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/phpfpm/docker-entrypoint.sh
@@ -181,8 +181,10 @@
 # Fix permissions for global filters
 chown -R 82:82 /global_sieve/*
 
-[[ ! -f /etc/nginx/conf.d/ZZZ-ejabberd.conf ]] && echo '# Autogenerated by mailcow' > /etc/nginx/conf.d/ZZZ-ejabberd.conf
-chown 82:82 /etc/nginx/conf.d/ZZZ-ejabberd.conf
+# Fix permissions on twig cache folder
+chown -R 82:82 /web/templates/cache
+# Clear cache
+find /web/templates/cache/* -not -name '.gitkeep' -delete
 
 # Run hooks
 for file in /hooks/*; do
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/postfix/postfix.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/postfix/postfix.sh
index 35cd790..e734a9a 100755
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/postfix/postfix.sh
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/postfix/postfix.sh
@@ -181,11 +181,31 @@
 dbname = ${DBNAME}
 query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
   WHERE id IN (
-    SELECT relayhost FROM domain
-      WHERE CONCAT('@', domain) = '%s'
-      OR domain IN (
-        SELECT target_domain FROM alias_domain WHERE CONCAT('@', alias_domain) =  '%s'
+    SELECT COALESCE(
+      (SELECT id FROM relayhosts
+      LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
+      WHERE relayhosts.active = '1'
+        AND (domain.domain = '%d'
+          OR domain.domain IN (
+            SELECT target_domain FROM alias_domain
+            WHERE alias_domain = '%d'
+          )
+        )
+      ),
+      (SELECT id FROM relayhosts
+      LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
+      WHERE relayhosts.active = '1'
+        AND (
+          mailbox.username IN (
+            SELECT alias.goto from alias
+              JOIN mailbox ON mailbox.username = alias.goto
+                WHERE alias.active = '1'
+                  AND alias.address = '%s'
+                  AND alias.address NOT LIKE '@%%'
+          )
+        )
       )
+    )
   )
   AND active = '1'
   AND username != '';
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/Dockerfile
index 888bdcb..ee08f8d 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/rspamd/Dockerfile
@@ -1,8 +1,8 @@
-FROM debian:buster-slim
-LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
+FROM debian:bullseye-slim
+LABEL maintainer "Andre Peters <andre.peters@tinc.gmbh>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG CODENAME=buster
+ARG CODENAME=bullseye
 ENV LC_ALL C
 
 RUN apt-get update && apt-get install -y \
@@ -15,12 +15,13 @@
   && 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 \
+  && apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
   && rm -rf /var/lib/apt/lists/* \
   && apt-get autoremove --purge \
   && apt-get clean \
   && mkdir -p /run/rspamd \
-  && chown _rspamd:_rspamd /run/rspamd
+  && chown _rspamd:_rspamd /run/rspamd \
+  && echo 'alias ll="ls -la --color"' >> ~/.bashrc
 
 COPY settings.conf /etc/rspamd/settings.conf
 COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/sogo/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/sogo/Dockerfile
index 9cf5f62..72f7d81 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/sogo/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/sogo/Dockerfile
@@ -1,10 +1,10 @@
-FROM debian:buster-slim
+FROM debian:bullseye-slim
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
 ARG SOGO_DEBIAN_REPOSITORY=http://packages.inverse.ca/SOGo/nightly/5/debian/
 ENV LC_ALL C
-ENV GOSU_VERSION 1.12
+ENV GOSU_VERSION 1.14
 
 # Prerequisites
 RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
@@ -31,7 +31,7 @@
   && mkdir /usr/share/doc/sogo \
   && touch /usr/share/doc/sogo/empty.sh \
   && apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \
-  && echo "deb ${SOGO_DEBIAN_REPOSITORY} buster buster" > /etc/apt/sources.list.d/sogo.list \
+  && echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
   && apt-get update && apt-get install -y --no-install-recommends \
     sogo \
     sogo-activesync \
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/unbound/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/unbound/Dockerfile
index cce2c00..a937e7e 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/unbound/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/unbound/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.13
+FROM alpine:3.14
 
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/Dockerfile
index e82bc5d..00cb8e9 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.11
+FROM alpine:3.14
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 
 # Installation
@@ -36,4 +36,4 @@
 COPY watchdog.sh /watchdog.sh
 COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
 
-CMD /watchdog.sh 2> /dev/null
+CMD /watchdog.sh
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh
index 66ab12e..086e326 100755
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh
@@ -6,7 +6,10 @@
 # Prepare
 BACKGROUND_TASKS=()
 echo "Waiting for containers to settle..."
-sleep 30
+for i in {30..1}; do
+  echo "${i}"
+  sleep 1
+done
 
 if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then
   echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..."
@@ -14,6 +17,14 @@
   exec $(readlink -f "$0")
 fi
 
+if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  SMTP_VERBOSE="--verbose"
+  set -xv
+else
+  SMTP_VERBOSE=""
+  exec 2>/dev/null
+fi
+
 # Checks pipe their corresponding container name in this pipe
 if [[ ! -p /tmp/com_pipe ]]; then
   mkfifo /tmp/com_pipe
@@ -114,16 +125,16 @@
   IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
   for rcpt in "${MAIL_RCPTS[@]}"; do
     RCPT_DOMAIN=
-    #RCPT_MX=
+    RCPT_MX=
     RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
-    # Latest smtp-cli looks up mx via dns
-    #RCPT_MX=$(dig +short ${RCPT_DOMAIN} mx | sort -n | awk '{print $2; exit}')
-    #if [[ -z ${RCPT_MX} ]]; then
-    #  log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
-    #  return 1
-    #fi
+    CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
+    if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
+      log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
+      return 1
+    fi
     [ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
     timeout 10s ./smtp-cli --missing-modules-ok \
+      "${SMTP_VERBOSE}" \
       --charset=UTF-8 \
       --subject="${SUBJECT}" \
       --body-plain="${BODY}" \
@@ -132,8 +143,15 @@
       --from="watchdog@${MAILCOW_HOSTNAME}" \
       --hello-host=${MAILCOW_HOSTNAME} \
       --ipv4
-      #--server="${RCPT_MX}"
-    log_msg "Sent notification email to ${rcpt}"
+    if [[ $? -eq 1 ]]; then # exit code 1 is fine
+      log_msg "Sent notification email to ${rcpt}"
+    else
+      if [[ "${SMTP_VERBOSE}" == "" ]]; then
+        log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
+      else
+        log_msg "Error while sending notification email to ${rcpt}."
+      fi
+    fi
   done
 }
 
@@ -154,7 +172,7 @@
       CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
       if [[ ! -z ${CONTAINER_ID} ]]; then
         for matched_container in "${CONTAINER_ID[@]}"; do
-          CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) 
+          CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
           for ip_match in "${CONTAINER_IPS[@]}"; do
             # grep will do nothing if one of these vars is empty
             [[ -z ${ip_match} ]] && continue
@@ -358,7 +376,7 @@
     touch /tmp/sogo-mailcow; echo "$(tail -50 /tmp/sogo-mailcow)" > /tmp/sogo-mailcow
     host_ip=$(get_container_ip sogo-mailcow)
     err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
+    /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
     [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
     progress "SOGo" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}