浏览代码

Merge branch 'master' of https://github.com/andryyy/mailcow-dockerized into recipient_map

Conflicts:
	data/web/inc/init_db.inc.php
Michael Kuron 7 年之前
父节点
当前提交
c30448c4d8
共有 42 个文件被更改,包括 712 次插入436 次删除
  1. 30 12
      data/Dockerfiles/clamd/Dockerfile
  2. 26 20
      data/Dockerfiles/dovecot/imapsync_cron.pl
  3. 62 58
      data/Dockerfiles/fail2ban/logwatch.py
  4. 6 16
      data/Dockerfiles/phpfpm/Dockerfile
  5. 3 1
      data/Dockerfiles/postfix/Dockerfile
  6. 1 1
      data/Dockerfiles/postfix/whitelist_forwardinghosts.sh
  7. 1 1
      data/Dockerfiles/rspamd/settings.conf
  8. 7 4
      data/Dockerfiles/sogo/bootstrap-sogo.sh
  9. 1 0
      data/Dockerfiles/watchdog/watchdog.sh
  10. 11 88
      data/assets/nextcloud/nextcloud.conf
  11. 47 0
      data/conf/clamav/clamd.conf
  12. 17 0
      data/conf/clamav/freshclam.conf
  13. 1 1
      data/conf/nginx/dynmaps.conf
  14. 31 16
      data/conf/nginx/site.conf
  15. 7 0
      data/conf/phpfpm/php-conf.d/opcache-recommended.ini
  16. 12 0
      data/conf/phpfpm/php-fpm.d/system.conf
  17. 11 0
      data/conf/phpfpm/php-fpm.d/www.conf
  18. 1 1
      data/conf/postfix/main.cf
  19. 2 2
      data/conf/rspamd/dynmaps/settings.php
  20. 1 1
      data/conf/rspamd/local.d/greylist.conf
  21. 1 1
      data/conf/rspamd/override.d/worker-controller.inc
  22. 2 2
      data/conf/sogo/sogo.conf
  23. 5 3
      data/conf/unbound/unbound.conf
  24. 47 28
      data/web/admin.php
  25. 19 0
      data/web/css/mailcow.css
  26. 26 1
      data/web/edit.php
  27. 3 1
      data/web/inc/footer.inc.php
  28. 12 0
      data/web/inc/functions.fail2ban.inc.php
  29. 9 2
      data/web/inc/functions.inc.php
  30. 50 16
      data/web/inc/functions.mailbox.inc.php
  31. 7 5
      data/web/inc/header.inc.php
  32. 9 5
      data/web/inc/init_db.inc.php
  33. 2 2
      data/web/js/quarantaine.js
  34. 11 0
      data/web/lang/lang.de.php
  35. 11 0
      data/web/lang/lang.en.php
  36. 21 0
      data/web/modals/mailbox.php
  37. 21 0
      data/web/modals/user.php
  38. 7 9
      data/web/user.php
  39. 33 30
      docker-compose.yml
  40. 8 0
      generate_config.sh
  41. 6 3
      helper-scripts/nextcloud.sh
  42. 124 106
      update.sh

+ 30 - 12
data/Dockerfiles/clamd/Dockerfile

@@ -6,20 +6,38 @@ LABEL maintainer "André Peters <andre.peters@servercow.de>"
 COPY dl_files.sh bootstrap.sh ./
 
 # Installation
-RUN apk add --update \
-	&& apk add --no-cache clamav clamav-libunrar curl bash tini \
+ENV CLAMAV 0.99.3
+
+RUN apk add --no-cache --virtual build-dependencies alpine-sdk ncurses-dev zlib-dev bzip2-dev pcre-dev linux-headers fts-dev libxml2-dev libressl-dev \
+  && apk add --no-cache curl bash tini libxml2 libbz2 pcre fts libressl \
+  && wget -O - https://www.clamav.net/downloads/production/clamav-${CLAMAV}.tar.gz | tar xfvz - \
+  && cd clamav-${CLAMAV} \
+  && LIBS=-lfts ./configure \
+  --prefix=/usr \
+  --libdir=/usr/lib \
+  --sysconfdir=/etc/clamav \
+  --mandir=/usr/share/man \
+  --infodir=/usr/share/info \
+  --without-iconv \
+  --disable-llvm \
+  --with-user=clamav \
+  --with-group=clamav \
+  --with-dbdir=/var/lib/clamav \
+  --enable-clamdtop \
+  --enable-bigstack \
+  --with-pcre \
+  && make -j4 \
+  && make install \
+  && make clean \
+  && cd .. && rm -rf clamav-${CLAMAV} \
+  && apk del build-dependencies \
+  && addgroup -S clamav \
+  && adduser -S -D -h /var/lib/clamav -s /sbin/nologin -G clamav -g clamav clamav \
+  && mkdir -p /run/clamav \
+  && chown clamav:clamav /run/clamav \
 	&& chmod +x /dl_files.sh \
 	&& set -ex; /bin/bash /dl_files.sh \
-	&& mkdir /run/clamav \
-	&& chown clamav:clamav /run/clamav \
-	&& chmod 750 /run/clamav \
-	&& sed -i '/Foreground yes/s/^#//g' /etc/clamav/clamd.conf \
-	&& sed -i '/TCPSocket 3310/s/^#//g' /etc/clamav/clamd.conf \
-  && sed -i 's#LogFile /var/log/clamav/clamd.log#LogFile /tmp/logpipe_clamd#g' /etc/clamav/clamd.conf \
-	&& sed -i 's/#PhishingSignatures yes/PhishingSignatures no/g' /etc/clamav/clamd.conf \
-	&& sed -i 's/#PhishingScanURLs yes/PhishingScanURLs no/g' /etc/clamav/clamd.conf \
-  && sed -i 's#UpdateLogFile /var/log/clamav/freshclam.log#UpdateLogFile /tmp/logpipe_freshclam#g' /etc/clamav/freshclam.conf \
-	&& sed -i '/Foreground yes/s/^#//g' /etc/clamav/freshclam.conf
+	&& chmod 750 /run/clamav
 
 # Port provision
 EXPOSE 3310

+ 26 - 20
data/Dockerfiles/dovecot/imapsync_cron.pl

@@ -33,26 +33,29 @@ open my $file, '<', "/etc/sogo/sieve.creds";
 my $creds = <$file>; 
 close $file;
 my ($master_user, $master_pass) = split /:/, $creds;
-my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1, delete2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL) ORDER BY last_run");
+my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1, delete2, automap, skipcrossduplicates, maxbytespersecond FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL) ORDER BY last_run");
 $sth->execute();
 my $row;
 
 while ($row = $sth->fetchrow_arrayref()) {
 
-  $id                 = @$row[0];
-  $user1              = @$row[1];
-  $user2              = @$row[2];
-  $host1              = @$row[3];
-  $authmech1          = @$row[4];
-  $password1          = @$row[5];
-  $exclude            = @$row[6];
-  $port1              = @$row[7];
-  $enc1               = @$row[8];
-  $delete2duplicates  = @$row[9];
-  $maxage             = @$row[10];
-  $subfolder2         = @$row[11];
-  $delete1            = @$row[12];
-  $delete2            = @$row[13];
+  $id                  = @$row[0];
+  $user1               = @$row[1];
+  $user2               = @$row[2];
+  $host1               = @$row[3];
+  $authmech1           = @$row[4];
+  $password1           = @$row[5];
+  $exclude             = @$row[6];
+  $port1               = @$row[7];
+  $enc1                = @$row[8];
+  $delete2duplicates   = @$row[9];
+  $maxage              = @$row[10];
+  $subfolder2          = @$row[11];
+  $delete1             = @$row[12];
+  $delete2             = @$row[13];
+  $automap             = @$row[14];
+  $skipcrossduplicates = @$row[15];
+  $maxbytespersecond   = @$row[16];
 
   $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?");
   $is_running->bind_param( 1, ${id} );
@@ -72,11 +75,14 @@ while ($row = $sth->fetchrow_arrayref()) {
 	"--tmpdir", "/tmp",
 	"--subscribeall",
 	($exclude eq ""	? () : ("--exclude", $exclude)),
-	($subfolder2 eq ""	? () : ('--subfolder2', $subfolder2)),
-	($maxage eq "0"	? () : ('--maxage', $maxage)),
-	($delete2duplicates	ne "1"	? () : ('--delete2duplicates')),
-	($delete1	ne "1"	? () : ('--delete')),
-    ($delete2   ne "1"  ? () : ('--delete2')),
+	($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
+	($maxage eq "0" ? () : ('--maxage', $maxage)),
+  ($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxage)),
+	($delete2duplicates	ne "1" ? () : ('--delete2duplicates')),
+	($delete1	ne "1" ? () : ('--delete')),
+  ($delete2 ne "1" ? () : ('--delete2')),
+  ($automap ne "1" ? () : ('--automap')),
+  ($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')),
 	(!defined($enc1) ? () : ($enc1)),
 	"--host1", $host1,
 	"--user1", $user1,

+ 62 - 58
data/Dockerfiles/fail2ban/logwatch.py

@@ -14,11 +14,11 @@ import json
 
 yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$')
 if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
-  print "SKIP_FAIL2BAN=y, Skipping Fail2ban container..."
+  print 'SKIP_FAIL2BAN=y, Skipping Fail2ban container...'
   time.sleep(31536000)
   raise SystemExit
 
-r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
+r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
 pubsub = r.pubsub()
 
 RULES = {}
@@ -29,19 +29,23 @@ RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=(
 RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
 RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
 
-r.setnx("F2B_BAN_TIME", "1800")
-r.setnx("F2B_MAX_ATTEMPTS", "10")
-r.setnx("F2B_RETRY_WINDOW", "600")
+r.setnx('F2B_BAN_TIME', '1800')
+r.setnx('F2B_MAX_ATTEMPTS', '10')
+r.setnx('F2B_RETRY_WINDOW', '600')
+r.setnx('F2B_NETBAN_IPV6', '64')
+r.setnx('F2B_NETBAN_IPV4', '24')
 
 bans = {}
 log = {}
 quit_now = False
 
 def ban(address):
-  BAN_TIME = int(r.get("F2B_BAN_TIME"))
-  MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
-  RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
-  WHITELIST = r.hgetall("F2B_WHITELIST")
+  BAN_TIME = int(r.get('F2B_BAN_TIME'))
+  MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS'))
+  RETRY_WINDOW = int(r.get('F2B_RETRY_WINDOW'))
+  WHITELIST = r.hgetall('F2B_WHITELIST')
+  NETBAN_IPV6 = '/' + str(r.get('F2B_NETBAN_IPV6'))
+  NETBAN_IPV4 = '/' + str(r.get('F2B_NETBAN_IPV4'))
 
   ip = ipaddress.ip_address(address.decode('ascii'))
   if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
@@ -56,13 +60,13 @@ def ban(address):
       wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
       if wl_net.overlaps(self_network):
         log['time'] = int(round(time.time()))
-        log['priority'] = "info"
-        log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
-        r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-        print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
+        log['priority'] = 'info'
+        log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
+        r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+        print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
         return
 
-  net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
+  net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
   net = str(net)
 
   if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
@@ -78,45 +82,45 @@ def ban(address):
 
   if bans[net]['attempts'] >= MAX_ATTEMPTS:
     log['time'] = int(round(time.time()))
-    log['priority'] = "crit"
-    log['message'] = "Banning %s" % net
-    r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-    print "Banning %s for %d minutes" % (net, BAN_TIME / 60)
+    log['priority'] = 'crit'
+    log['message'] = 'Banning %s' % net
+    r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+    print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
     if type(ip) is ipaddress.IPv4Address:
-      subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
-      subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
+      subprocess.call(['iptables', '-I', 'INPUT', '-s', net, '-j', 'REJECT'])
+      subprocess.call(['iptables', '-I', 'FORWARD', '-s', net, '-j', 'REJECT'])
     else:
-      subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
-      subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
-    r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
+      subprocess.call(['ip6tables', '-I', 'INPUT', '-s', net, '-j', 'REJECT'])
+      subprocess.call(['ip6tables', '-I', 'FORWARD', '-s', net, '-j', 'REJECT'])
+    r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
   else:
     log['time'] = int(round(time.time()))
-    log['priority'] = "warn"
-    log['message'] = "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
-    r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-    print "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
+    log['priority'] = 'warn'
+    log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
+    r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+    print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
 
 def unban(net):
   log['time'] = int(round(time.time()))
-  log['priority'] = "info"
-  r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+  log['priority'] = 'info'
+  r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
   if not net in bans:
-    log['message'] = "%s is not banned, skipping unban and deleting from queue (if any)" % net
-    r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-    print "%s is not banned, skipping unban and deleting from queue (if any)" % net
-    r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
+    log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
+    r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+    print '%s is not banned, skipping unban and deleting from queue (if any)' % net
+    r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
     return
-  log['message'] = "Unbanning %s" % net
-  r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-  print "Unbanning %s" % net
+  log['message'] = 'Unbanning %s' % net
+  r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+  print 'Unbanning %s' % net
   if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
-    subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
-    subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
+    subprocess.call(['iptables', '-D', 'INPUT', '-s', net, '-j', 'REJECT'])
+    subprocess.call(['iptables', '-D', 'FORWARD', '-s', net, '-j', 'REJECT'])
   else:
-    subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
-    subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
-  r.hdel("F2B_ACTIVE_BANS", "%s" % net)
-  r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
+    subprocess.call(['ip6tables', '-D', 'INPUT', '-s', net, '-j', 'REJECT'])
+    subprocess.call(['ip6tables', '-D', 'FORWARD', '-s', net, '-j', 'REJECT'])
+  r.hdel('F2B_ACTIVE_BANS', '%s' % net)
+  r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
   del bans[net]
 
 def quit(signum, frame):
@@ -125,21 +129,21 @@ def quit(signum, frame):
 
 def clear():
   log['time'] = int(round(time.time()))
-  log['priority'] = "info"
-  log['message'] = "Clearing all bans"
-  r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-  print "Clearing all bans"
+  log['priority'] = 'info'
+  log['message'] = 'Clearing all bans'
+  r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+  print 'Clearing all bans'
   for net in bans.copy():
     unban(net)
   pubsub.unsubscribe()
 
 def watch():
   log['time'] = int(round(time.time()))
-  log['priority'] = "info"
-  log['message'] = "Watching Redis channel F2B_CHANNEL"
-  r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
-  pubsub.subscribe("F2B_CHANNEL")
-  print "Subscribing to Redis channel F2B_CHANNEL"
+  log['priority'] = 'info'
+  log['message'] = 'Watching Redis channel F2B_CHANNEL'
+  r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
+  pubsub.subscribe('F2B_CHANNEL')
+  print 'Subscribing to Redis channel F2B_CHANNEL'
   while True:
     for item in pubsub.listen():
       for rule_id, rule_regex in RULES.iteritems():
@@ -150,18 +154,18 @@ def watch():
             ip = ipaddress.ip_address(addr.decode('ascii'))
             if ip.is_private or ip.is_loopback:
               continue
-            print "%s matched rule id %d" % (addr, rule_id)
+            print '%s matched rule id %d' % (addr, rule_id)
             log['time'] = int(round(time.time()))
-            log['priority'] = "warn"
-            log['message'] = "%s matched rule id %d" % (addr, rule_id)
-            r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+            log['priority'] = 'warn'
+            log['message'] = '%s matched rule id %d' % (addr, rule_id)
+            r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False))
             ban(addr)
 
 def autopurge():
   while not quit_now:
-    BAN_TIME = int(r.get("F2B_BAN_TIME"))
-    MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
-    QUEUE_UNBAN = r.hgetall("F2B_QUEUE_UNBAN")
+    BAN_TIME = int(r.get('F2B_BAN_TIME'))
+    MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS'))
+    QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
     if QUEUE_UNBAN:
       for net in QUEUE_UNBAN:
         unban(str(net))

+ 6 - 16
data/Dockerfiles/phpfpm/Dockerfile

@@ -1,10 +1,11 @@
 FROM php:7.1-fpm-alpine
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
-ENV REDIS_PECL 3.1.4
-ENV MEMCACHED_PECL 3.0.3
-ENV APCU_PECL 5.1.8
+ENV REDIS_PECL 3.1.6
+ENV MEMCACHED_PECL 3.0.4
+ENV APCU_PECL 5.1.9
 ENV IMAGICK_PECL 3.4.3
+ENV MAILPARSE_PECL 3.0.2
 
 RUN apk add -U --no-cache libxml2-dev \
 	icu-dev \
@@ -41,27 +42,16 @@ RUN apk add -U --no-cache libxml2-dev \
     Net_Sieve \
     NET_SMTP \
     Mail_mime \
-	&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} mailparse \
+	&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} mailparse-${MAILPARSE_PECL} \
 	&& docker-php-ext-enable redis apcu memcached imagick mailparse \
 	&& pecl clear-cache \
 	&& docker-php-ext-configure intl \
   && docker-php-ext-install -j 4 intl gettext ldap sockets soap pdo pdo_mysql xmlrpc gd zip pcntl opcache \
   && docker-php-ext-configure imap --with-imap --with-imap-ssl \
 	&& docker-php-ext-install -j 4 imap \
-	&& apk del --purge autoconf g++ make libxml2-dev icu-dev imap-dev openssl-dev cyrus-sasl-dev pcre-dev libpng-dev libpng-dev libjpeg-turbo-dev libwebp-dev zlib-dev imagemagick-dev \
-	&& { \
-  echo 'opcache.enable=1'; \
-  echo 'opcache.enable_cli=1'; \
-  echo 'opcache.interned_strings_buffer=8'; \
-  echo 'opcache.max_accelerated_files=10000'; \
-  echo 'opcache.memory_consumption=128'; \
-  echo 'opcache.save_comments=1'; \
-  echo 'opcache.revalidate_freq=1'; \
-} > /usr/local/etc/php/conf.d/opcache-recommended.ini
+	&& apk del --purge autoconf g++ make libxml2-dev icu-dev imap-dev openssl-dev cyrus-sasl-dev pcre-dev libpng-dev libpng-dev libjpeg-turbo-dev libwebp-dev zlib-dev imagemagick-dev
 
 COPY ./docker-entrypoint.sh /
 
-EXPOSE 9000
-
 ENTRYPOINT ["/docker-entrypoint.sh"]
 CMD ["php-fpm"]

+ 3 - 1
data/Dockerfiles/postfix/Dockerfile

@@ -26,7 +26,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
 	syslog-ng-core \
 	syslog-ng-mod-redis \
 	&& rm -rf /var/lib/apt/lists/* \
-	&& touch /etc/default/locale
+	&& touch /etc/default/locale \
+  && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
+  && chmod +x /usr/local/sbin/postconf
 
 RUN addgroup --system --gid 600 zeyple
 RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple

+ 1 - 1
data/Dockerfiles/postfix/whitelist_forwardinghosts.sh

@@ -6,7 +6,7 @@ while read QUERY; do
 		echo "500 dunno"
 		continue
 	fi
-	result=$(curl -s http://172.22.1.251:8081/forwardinghosts.php?host=${QUERY[1]})
+	result=$(curl -s http://nginx:8081/forwardinghosts.php?host=${QUERY[1]})
 	logger -t whitelist_forwardinghosts -p mail.info "Look up ${QUERY[1]} on whitelist, result $result"
 	echo ${result}
 done

+ 1 - 1
data/Dockerfiles/rspamd/settings.conf

@@ -1 +1 @@
-settings = "http://172.22.1.251:8081/settings.php";
+settings = "http://nginx:8081/settings.php";

+ 7 - 4
data/Dockerfiles/sogo/bootstrap-sogo.sh

@@ -18,11 +18,13 @@ done
 mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
 
 mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, home, kind, multiple_bookings) AS
-SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox
-LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username = mailbox.username
+CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, sa_aliases, ad_aliases, home, kind, multiple_bookings) AS
+SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gsa.send_as_acl, ''), IFNULL(gda.ad_alias, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox
+LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
+LEFT OUTER JOIN grouped_sender_acl gsa ON gsa.username = mailbox.username
 LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
-WHERE mailbox.active = '1';
+WHERE mailbox.active = '1'
+GROUP BY mailbox.username;
 EOF
 
 
@@ -67,6 +69,7 @@ while read line
                     <key>MailFieldNames</key>
                     <array>
                         <string>aliases</string>
+                        <string>sa_aliases</string>
                         <string>ad_aliases</string>
                     </array>
                     <key>KindFieldName</key>

+ 1 - 0
data/Dockerfiles/watchdog/watchdog.sh

@@ -191,6 +191,7 @@ phpfpm_checks() {
     host_ip=$(get_container_ip php-fpm-mailcow)
     err_c_cur=${err_count}
     cgi-fcgi -bind -connect ${host_ip}:9000 | grep "Content-type" 1>&2; err_count=$(( ${err_count} + ($? * 2)))
+    cgi-fcgi -bind -connect ${host_ip}:9001 | grep "Content-type" 1>&2; err_count=$(( ${err_count} + ($? * 2)))
     /usr/lib/nagios/plugins/check_ping -4 -H ${host_ip} -w 2000,10% -c 4000,100% -p2 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} ))

+ 11 - 88
data/assets/nextcloud/nextcloud.conf

@@ -25,7 +25,7 @@ server {
   add_header X-Download-Options noopen;
   add_header X-Permitted-Cross-Domain-Policies none;
 
-  server_name NC_SERVER_SUB;
+  server_name NC_SUBD;
 
   root /web/nextcloud/;
 
@@ -38,95 +38,14 @@ server {
   location = /.well-known/carddav {
     return 301 $client_req_scheme_nc://$host/remote.php/dav;
   }
+
   location = /.well-known/caldav {
     return 301 $client_req_scheme_nc://$host/remote.php/dav;
   }
 
-  gzip on;
-  gzip_vary on;
-  gzip_comp_level 4;
-  gzip_min_length 256;
-  gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
-  gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
-
-  set_real_ip_from 172.22.1.1;
-  real_ip_header X-Forwarded-For;
-  real_ip_recursive on;
-
-  location / {
-    rewrite ^ /index.php$uri;
-  }
-
-  location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {
-    deny all;
-  }
-
-  location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {
-    deny all;
-  }
-
-  location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
-    fastcgi_split_path_info ^(.+\.php)(/.*)$;
-    include fastcgi_params;
-    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
-    fastcgi_param PATH_INFO $fastcgi_path_info;
-    #Avoid sending the security headers twice
-    fastcgi_param modHeadersAvailable true;
-    fastcgi_param front_controller_active true;
-    fastcgi_pass phpfpm:9000;
-    fastcgi_intercept_errors on;
-    fastcgi_request_buffering off;
-    client_max_body_size 10G;
-  }
-
-  location ~ ^/(?:updater|ocs-provider)(?:$|/) {
-    try_files $uri/ =404;
-    index index.php;
-  }
-
-  location ~ \.(?:css|js|woff|svg|gif)$ {
-    try_files $uri /index.php$uri$is_args$args;
-    add_header Cache-Control "public, max-age=15778463";
-    add_header X-Content-Type-Options nosniff;
-    add_header X-XSS-Protection "1; mode=block";
-    add_header X-Robots-Tag none;
-    add_header X-Download-Options noopen;
-    add_header X-Permitted-Cross-Domain-Policies none;
-    access_log off;
-  }
-
-  location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
-    try_files $uri /index.php$uri$is_args$args;
-    access_log off;
-  }
-}
-server {
-  include /etc/nginx/conf.d/listen_ssl.active;
-  include /etc/nginx/mime.types;
-  charset utf-8;
-  override_charset on;
-
-  add_header X-Content-Type-Options nosniff;
-  add_header X-XSS-Protection "1; mode=block";
-  add_header X-Robots-Tag none;
-  add_header X-Download-Options noopen;
-  add_header X-Permitted-Cross-Domain-Policies none;
-
-  server_name NC_SERVER_SUB;
-
-  root /web/nextcloud/;
-
-  location = /robots.txt {
-    allow all;
-    log_not_found off;
-    access_log off;
-  }
-
-  location = /.well-known/carddav {
-    return 301 $client_req_scheme_nc://$host/remote.php/dav;
-  }
-  location = /.well-known/caldav {
-    return 301 $client_req_scheme_nc://$host/remote.php/dav;
+  location ^~ /.well-known/acme-challenge/ {
+    default_type "text/plain";
+    root /web;
   }
 
   gzip on;
@@ -135,8 +54,10 @@ server {
   gzip_min_length 256;
   gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
   gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
-
-  set_real_ip_from 172.22.1.1;
+  set_real_ip_from fd00::/8;
+  set_real_ip_from 10.0.0.0/8;
+  set_real_ip_from 172.16.0.0/12;
+  set_real_ip_from 192.168.0.0/16;
   real_ip_header X-Forwarded-For;
   real_ip_recursive on;
 
@@ -157,11 +78,13 @@ server {
     include fastcgi_params;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     fastcgi_param PATH_INFO $fastcgi_path_info;
+    #Avoid sending the security headers twice
     fastcgi_param modHeadersAvailable true;
     fastcgi_param front_controller_active true;
     fastcgi_pass phpfpm:9000;
     fastcgi_intercept_errors on;
     fastcgi_request_buffering off;
+    client_max_body_size 10G;
   }
 
   location ~ ^/(?:updater|ocs-provider)(?:$|/) {

+ 47 - 0
data/conf/clamav/clamd.conf

@@ -0,0 +1,47 @@
+LogFile /tmp/logpipe_clamd
+LogTime yes
+LogClean yes
+ExtendedDetectionInfo yes
+PidFile /run/clamav/clamd.pid
+OfficialDatabaseOnly no
+LocalSocket /run/clamav/clamd.sock
+TCPSocket 3310
+StreamMaxLength 25M
+MaxThreads 10
+ReadTimeout 10
+CommandReadTimeout 3
+SendBufTimeout 200
+MaxQueue 80
+IdleTimeout 20
+SelfCheck 3600
+User clamav
+AllowSupplementaryGroups yes
+Foreground yes
+DetectPUA yes
+# See https://github.com/vrtadmin/clamav-faq/blob/master/faq/faq-pua.md
+#ExcludePUA NetTool
+#ExcludePUA PWTool
+#IncludePUA Spy
+#IncludePUA Scanner
+#IncludePUA RAT
+AlgorithmicDetection yes
+ScanOLE2 yes
+OLE2BlockMacros yes
+ScanPDF yes
+ScanSWF yes
+ScanXMLDOCS yes
+ScanHWP3 yes
+ScanMail yes
+PhishingSignatures no
+PhishingScanURLs no
+HeuristicScanPrecedence yes
+ScanHTML yes
+ScanArchive yes
+MaxScanSize 50M
+MaxFileSize 25M
+MaxRecursion 5
+MaxFiles 200
+ScanOnAccess no
+Bytecode yes
+BytecodeSecurity TrustSigned
+BytecodeTimeout 1000

+ 17 - 0
data/conf/clamav/freshclam.conf

@@ -0,0 +1,17 @@
+UpdateLogFile /tmp/logpipe_freshclam
+LogTime yes
+PidFile /run/clamav/freshclam.pid
+DatabaseOwner clamav
+AllowSupplementaryGroups yes
+DNSDatabaseInfo current.cvd.clamav.net
+DatabaseMirror database.clamav.net
+MaxAttempts 4
+ScriptedUpdates yes
+Checks 6
+NotifyClamd /etc/clamav/clamd.conf
+Foreground yes
+ConnectTimeout 20
+ReceiveTimeout 20
+TestDatabases yes
+Bytecode yes
+

+ 1 - 1
data/conf/nginx/dynmaps.conf

@@ -10,7 +10,7 @@ server {
   location ~ \.php$ {
     try_files $uri =404;
     fastcgi_split_path_info ^(.+\.php)(/.+)$;
-    fastcgi_pass phpfpm:9000;
+    fastcgi_pass phpfpm:9001;
     fastcgi_index index.php;
     include fastcgi_params;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

+ 31 - 16
data/conf/nginx/site.conf

@@ -7,6 +7,13 @@ map $http_x_forwarded_proto $client_req_scheme {
      https https;
 }
 
+server {
+  listen 80 default_server;
+  listen [::]:80 default_server;
+  include /etc/nginx/conf.d/server_name.active;
+  return 301 https://$host$request_uri;
+}
+
 server {
   include /etc/nginx/mime.types;
   charset utf-8;
@@ -17,6 +24,7 @@ server {
   add_header X-XSS-Protection "1; mode=block";
   add_header X-Robots-Tag none;
   add_header X-Download-Options noopen;
+  add_header X-Frame-Options "SAMEORIGIN";
   add_header X-Permitted-Cross-Domain-Policies none;
 
   index index.php index.html;
@@ -39,7 +47,10 @@ server {
   }
 
   # If behind reverse proxy, forwards the correct IP
-  set_real_ip_from 172.22.1.1;
+  set_real_ip_from 10.0.0.0/8;
+  set_real_ip_from 172.16.0.0/12;
+  set_real_ip_from 192.168.0.0/16;
+  set_real_ip_from fd00::/8;
   real_ip_header X-Forwarded-For;
   real_ip_recursive on;
 
@@ -65,7 +76,7 @@ server {
   }
 
   location /rspamd/ {
-    proxy_pass       http://172.22.1.253:11334/;
+    proxy_pass       http://rspamd:11334/;
     proxy_set_header Host      $http_host;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Real-IP $remote_addr;
@@ -97,7 +108,7 @@ server {
   }
 
   location ^~ /Microsoft-Server-ActiveSync {
-    proxy_pass http://172.22.1.252:20000/SOGo/Microsoft-Server-ActiveSync;
+    proxy_pass http://sogo:20000/SOGo/Microsoft-Server-ActiveSync;
     proxy_connect_timeout 1000;
     proxy_next_upstream timeout error;
     proxy_send_timeout 1000;
@@ -119,7 +130,7 @@ server {
   }
 
   location ^~ /SOGo {
-    proxy_pass http://172.22.1.252:20000;
+    proxy_pass http://sogo:20000;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header Host $http_host;
@@ -134,7 +145,7 @@ server {
   }
 
   location /SOGo.woa/WebServerResources/ {
-    proxy_pass http://172.22.1.252:9192/WebServerResources/;
+    proxy_pass http://sogo:9192/WebServerResources/;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -144,7 +155,7 @@ server {
   }
 
   location /.woa/WebServerResources/ {
-    proxy_pass http://172.22.1.252:9192/WebServerResources/;
+    proxy_pass http://sogo:9192/WebServerResources/;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -154,7 +165,7 @@ server {
   }
 
   location /SOGo/WebServerResources/ {
-    proxy_pass http://172.22.1.252:9192/WebServerResources/;
+    proxy_pass http://sogo:9192/WebServerResources/;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -164,7 +175,7 @@ server {
   }
 
   location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ {
-    proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2;
+    proxy_pass http://sogo:9192/$1.SOGo/Resources/$2;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -195,6 +206,7 @@ server {
   add_header X-XSS-Protection "1; mode=block";
   add_header X-Robots-Tag none;
   add_header X-Download-Options noopen;
+  add_header X-Frame-Options "SAMEORIGIN";
   add_header X-Permitted-Cross-Domain-Policies none;
 
   index index.php index.html;
@@ -217,7 +229,10 @@ server {
   }
 
   # If behind reverse proxy, forwards the correct IP
-  set_real_ip_from 172.22.1.1;
+  set_real_ip_from 10.0.0.0/8;
+  set_real_ip_from 172.16.0.0/12;
+  set_real_ip_from 192.168.0.0/16;
+  set_real_ip_from fd00::/8;
   real_ip_header X-Forwarded-For;
   real_ip_recursive on;
 
@@ -243,7 +258,7 @@ server {
   }
 
   location /rspamd/ {
-    proxy_pass       http://172.22.1.253:11334/;
+    proxy_pass       http://rspamd:11334/;
     proxy_set_header Host      $http_host;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Real-IP $remote_addr;
@@ -275,7 +290,7 @@ server {
   }
 
   location ^~ /Microsoft-Server-ActiveSync {
-    proxy_pass http://172.22.1.252:20000/SOGo/Microsoft-Server-ActiveSync;
+    proxy_pass http://sogo:20000/SOGo/Microsoft-Server-ActiveSync;
     proxy_connect_timeout 1000;
     proxy_next_upstream timeout error;
     proxy_send_timeout 1000;
@@ -297,7 +312,7 @@ server {
   }
 
   location ^~ /SOGo {
-    proxy_pass http://172.22.1.252:20000;
+    proxy_pass http://sogo:20000;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header Host $http_host;
@@ -312,7 +327,7 @@ server {
   }
 
   location /SOGo.woa/WebServerResources/ {
-    proxy_pass http://172.22.1.252:9192/WebServerResources/;
+    proxy_pass http://sogo:9192/WebServerResources/;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -322,7 +337,7 @@ server {
   }
 
   location /.woa/WebServerResources/ {
-    proxy_pass http://172.22.1.252:9192/WebServerResources/;
+    proxy_pass http://sogo:9192/WebServerResources/;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -332,7 +347,7 @@ server {
   }
 
   location /SOGo/WebServerResources/ {
-    proxy_pass http://172.22.1.252:9192/WebServerResources/;
+    proxy_pass http://sogo:9192/WebServerResources/;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;
@@ -342,7 +357,7 @@ server {
   }
 
   location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ {
-    proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2;
+    proxy_pass http://sogo:9192/$1.SOGo/Resources/$2;
     proxy_set_header Host $http_host;
     proxy_cache sogo;
     proxy_cache_valid 200 1d;

+ 7 - 0
data/conf/phpfpm/php-conf.d/opcache-recommended.ini

@@ -0,0 +1,7 @@
+opcache.enable=1
+opcache.enable_cli=1
+opcache.interned_strings_buffer=8
+opcache.max_accelerated_files=10000
+opcache.memory_consumption=128
+opcache.save_comments=1
+opcache.revalidate_freq=1

+ 12 - 0
data/conf/phpfpm/php-fpm.d/system.conf

@@ -0,0 +1,12 @@
+[system]
+user = www-data
+group = www-data
+pm = dynamic
+pm.max_children = 10
+pm.start_servers = 2
+pm.min_spare_servers = 2
+pm.max_spare_servers = 4
+listen = [::]:9001
+access.log = /proc/self/fd/2
+clear_env = no
+catch_workers_output = yes

+ 11 - 0
data/conf/phpfpm/php-fpm.d/www.conf

@@ -0,0 +1,11 @@
+[www]
+user = www-data
+group = www-data
+pm = ondemand
+pm.max_children = 20
+pm.process_idle_timeout = 20s
+pm.max_requests = 800
+listen = [::]:9000
+access.log = /proc/self/fd/2
+clear_env = no
+catch_workers_output = yes

+ 1 - 1
data/conf/postfix/main.cf

@@ -9,7 +9,7 @@ smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_una
 alias_maps = hash:/etc/aliases
 alias_database = hash:/etc/aliases
 relayhost =
-mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 [fd4d:6169:6c63:6f77::]/64
+mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 [fd::]/8
 mailbox_size_limit = 0
 recipient_delimiter = +
 inet_interfaces = all

+ 2 - 2
data/conf/rspamd/dynmaps/settings.php

@@ -196,7 +196,7 @@ while ($row = array_shift($rows)) {
 	}
 	whitelist_header_<?=$username_sane;?> {
 <?php
-	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(CONCAT('^', `value`, '$'), '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
+	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(CONCAT('\<', `value`, '\>'), '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
 		WHERE `object`= :object
 			AND `option` = 'whitelist_from'");
 	$stmt->execute(array(':object' => $row['object']));
@@ -288,7 +288,7 @@ while ($row = array_shift($rows)) {
 	}
 	blacklist_header_<?=$username_sane;?> {
 <?php
-	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(CONCAT('^', `value`, '$'), '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
+	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(CONCAT('\<', `value`, '\>'), '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
 		WHERE `object`= :object
 			AND `option` = 'blacklist_from'");
 	$stmt->execute(array(':object' => $row['object']));

+ 1 - 1
data/conf/rspamd/local.d/greylist.conf

@@ -1 +1 @@
-whitelisted_ip = "http://172.22.1.251:8081/forwardinghosts.php";
+whitelisted_ip = "http://nginx:8081/forwardinghosts.php";

+ 1 - 1
data/conf/rspamd/override.d/worker-controller.inc

@@ -4,6 +4,6 @@ secure_ip = "172.16.0.0/12";
 secure_ip = "10.0.0.0/8";
 secure_ip = "127.0.0.1";
 secure_ip = "::1";
-secure_ip = "fd4d:6169:6c63:6f77::/64"
+secure_ip = "fd00::/8"
 .include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc"
 .include(try=true; priority=20) "$CONFDIR/override.d/worker-controller.custom.inc" 

+ 2 - 2
data/conf/sogo/sogo.conf

@@ -5,7 +5,7 @@
         PrivateDAndTViewer
     );
 
-    WOWorkersCount = "20";
+    WOWorkersCount = "7";
     SOGoACLsSendEMailNotifications = YES;
     SOGoAppointmentSendEMailNotifications = YES;
     SOGoDraftsFolderName = "Drafts";
@@ -36,7 +36,7 @@
     SOGoMailingMechanism = smtp;
     SOGoSMTPAuthenticationType = plain;
 
-    SxVMemLimit = 512;
+    SxVMemLimit = 384;
 
     SOGoMaximumPingInterval = 354;
 

+ 5 - 3
data/conf/unbound/unbound.conf

@@ -8,8 +8,11 @@ server:
   do-udp: yes
   do-tcp: yes
   do-daemonize: no
-  access-control: 172.22.1.0/24 allow
-  access-control: fd4d:6169:6c63:6f77::/64 allow
+  access-control: 10.0.0.0/8 allow
+  access-control: 172.16.0.0/12 allow
+  access-control: 192.168.0.0/16 allow
+  access-control: fd00::/8 allow
+  access-control: fe80::/10 allow
   directory: "/etc/unbound"
   username: unbound
   auto-trust-anchor-file: trusted-key.key
@@ -19,7 +22,6 @@ server:
   private-address: 169.254.0.0/16
   private-address: fd00::/8
   private-address: fe80::/10
-  private-address: fd4d:6169:6c63:6f77::/64
   root-hints: "/etc/unbound/root.hints"
   hide-identity: yes
   hide-version: yes

+ 47 - 28
data/web/admin.php

@@ -168,29 +168,31 @@ $tfa_data = get_tfa();
             if (!empty($dkim = dkim('details', $domain))) {
           ?>
             <div class="row">
-              <div class="col-xs-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" /></div>
-              <div class="col-xs-2">
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" /></div>
+              <div class="col-md-3">
                 <p>Domain: <strong><?=htmlspecialchars($domain);?></strong>
-                  <p><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
-                  <p><span class="label label-primary">Selector '<?=$dkim['dkim_selector'];?>'</span></p>
-                  <p><span class="label label-info"><?=$dkim['length'];?> bit</span></p>
+                  <p class="dkim-label"><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
+                  <p class="dkim-label"><span class="label label-primary">Selector '<?=$dkim['dkim_selector'];?>'</span></p>
+                  <p class="dkim-label"><span class="label label-info"><?=$dkim['length'];?> bit</span></p>
                 </p>
               </div>
-              <div class="col-xs-9">
+              <div class="col-md-8">
                   <pre><?=$dkim['dkim_txt'];?></pre>
                   <p data-toggle="modal" data-target="#showDKIMprivKey" id="dkim_priv" style="cursor:pointer;margin-top:-8pt" data-priv-key="<?=$dkim['privkey'];?>"><small>↪ Private key</small></p>
               </div>
+              <hr class="visible-xs visible-sm">
             </div>
           <?php
           }
           else {
           ?>
           <div class="row">
-              <div class="col-xs-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
-            <div class="col-xs-2">
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
+            <div class="col-md-3">
               <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
             </div>
-            <div class="col-xs-9"><pre>-</pre></div>
+            <div class="col-md-8"><pre>-</pre></div>
+              <hr class="visible-xs visible-sm">
           </div>
           <?php
           }
@@ -198,29 +200,31 @@ $tfa_data = get_tfa();
             if (!empty($dkim = dkim('details', $alias_domain))) {
             ?>
               <div class="row">
-              <div class="col-xs-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" /></div>
-                <div class="col-xs-1 col-xs-offset-1">
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" /></div>
+                <div class="col-md-2 col-md-offset-1">
                   <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong></small>
-                    <p><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
-                    <p><span class="label label-primary">Selector '<?=$dkim['dkim_selector'];?>'</span></p>
-                    <p><span class="label label-info"><?=$dkim['length'];?> bit</span></p>
+                    <p class="dkim-label"><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
+                    <p class="dkim-label"><span class="label label-primary">Selector '<?=$dkim['dkim_selector'];?>'</span></p>
+                    <p class="dkim-label"><span class="label label-info"><?=$dkim['length'];?> bit</span></p>
                 </p>
                 </div>
-                <div class="col-xs-9">
+                <div class="col-md-8">
                   <pre><?=$dkim['dkim_txt'];?></pre>
                   <p data-toggle="modal" data-target="#showDKIMprivKey" id="dkim_priv" style="cursor:pointer;margin-top:-8pt" data-priv-key="<?=$dkim['privkey'];?>"><small>↪ Private key</small></p>
                 </div>
+              <hr class="visible-xs visible-sm">
               </div>
             <?php
             }
             else {
             ?>
             <div class="row">
-              <div class="col-xs-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
-              <div class="col-xs-1 col-xs-offset-1">
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
+              <div class="col-md-2 col-md-offset-1">
                 <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
               </div>
-              <div class="col-xs-9"><pre>-</pre></div>
+              <div class="col-md-8"><pre>-</pre></div>
+              <hr class="visible-xs visible-sm">
             </div>
             <?php
             }
@@ -230,18 +234,19 @@ $tfa_data = get_tfa();
           if (!empty($dkim = dkim('details', $blind))) {
           ?>
             <div class="row">
-              <div class="col-xs-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$blind;?>" /></div>
-              <div class="col-xs-2">
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$blind;?>" /></div>
+              <div class="col-md-3">
                 <p>Domain: <strong><?=htmlspecialchars($blind);?></strong>
-                  <p><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
-                  <p><span class="label label-primary">Selector '<?=$dkim['dkim_selector'];?>'</span></p>
-                  <p><span class="label label-info"><?=$dkim['length'];?> bit</span></p>
+                  <p class="dkim-label"><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
+                  <p class="dkim-label"><span class="label label-primary">Selector '<?=$dkim['dkim_selector'];?>'</span></p>
+                  <p class="dkim-label"><span class="label label-info"><?=$dkim['length'];?> bit</span></p>
                 </p>
                 </div>
-                <div class="col-xs-9">
+                <div class="col-md-8">
                   <pre><?=$dkim['dkim_txt'];?></pre>
                   <p data-toggle="modal" data-target="#showDKIMprivKey" id="dkim_priv" style="cursor:pointer;margin-top:-8pt" data-priv-key="<?=$dkim['privkey'];?>"><small>↪ Private key</small></p>
                 </div>
+                <hr class="visible-xs visible-sm">
             </div>
           <?php
           }
@@ -346,6 +351,20 @@ $tfa_data = get_tfa();
             <label for="retry_window"><?=$lang['admin']['f2b_retry_window'];?>:</label>
             <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
           </div>
+          <div class="form-group">
+            <label for="netban_ipv4"><?=$lang['admin']['f2b_netban_ipv4'];?>:</label>
+            <div class="input-group">
+              <span class="input-group-addon">/</span>
+              <input type="number" class="form-control" id="netban_ipv4" name="netban_ipv4" value="<?=$f2b_data['netban_ipv4'];?>" required>
+            </div>
+          </div>
+          <div class="form-group">
+            <label for="netban_ipv6"><?=$lang['admin']['f2b_netban_ipv6'];?>:</label>
+            <div class="input-group">
+              <span class="input-group-addon">/</span>
+              <input type="number" class="form-control" id="netban_ipv6" name="netban_ipv6" value="<?=$f2b_data['netban_ipv6'];?>" required>
+            </div>
+          </div>
           <div class="form-group">
             <label for="whitelist"><?=$lang['admin']['f2b_whitelist'];?>:</label>
             <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
@@ -427,7 +446,7 @@ $tfa_data = get_tfa();
               ?>
             </select>
           </div>
-          <button class="btn btn-success" id="edit_selected" data-item="self" data-id="quarantaine" data-api-url='edit/quarantaine' data-api-attr='{"action":"settings"}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+          <button class="btn btn-default" id="edit_selected" data-item="self" data-id="quarantaine" data-api-url='edit/quarantaine' data-api-attr='{"action":"settings"}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
         </form>
       </div>
     </div>
@@ -441,7 +460,7 @@ $tfa_data = get_tfa();
         <form class="form-inline" role="form" method="post" enctype="multipart/form-data">
           <p>
             <input type="file" name="main_logo" class="filestyle" data-buttonName="btn-default" data-buttonText="Select" accept="image/gif, image/jpeg, image/pjpeg, image/x-png, image/png, image/svg+xml">
-            <button name="submit_main_logo" type="submit" class="btn btn-success"><span class="glyphicon glyphicon-cloud-upload"></span> <?=$lang['admin']['upload'];?></button>
+            <button name="submit_main_logo" type="submit" class="btn btn-default"><span class="glyphicon glyphicon-cloud-upload"></span> <?=$lang['admin']['upload'];?></button>
           </p>
         </form>
         <?php
@@ -501,7 +520,7 @@ $tfa_data = get_tfa();
             ?>
           </table>
           <p><div class="btn-group">
-            <button class="btn btn-sm btn-success" id="edit_selected" data-item="admin" data-id="app_links" data-reload="no" data-api-url='edit/app_links' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+            <button class="btn btn-sm btn-default" id="edit_selected" data-item="admin" data-id="app_links" data-reload="no" data-api-url='edit/app_links' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
             <button class="btn btn-sm btn-default" type="button" id="add_app_link_row"><?=$lang['admin']['add_row'];?></button>
           </div></p>
         </form>
@@ -526,7 +545,7 @@ $tfa_data = get_tfa();
             <label for="help_text"><?=$lang['admin']['help_text'];?>:</label>
             <textarea class="form-control" id="help_text" name="help_text" rows="7"><?=$ui_texts['help_text'];?></textarea>
           </div>
-          <button class="btn btn-success" id="edit_selected" data-item="ui" data-id="uitexts" data-api-url='edit/ui_texts' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+          <button class="btn btn-default" id="edit_selected" data-item="ui" data-id="uitexts" data-api-url='edit/ui_texts' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
         </form>
       </div>
     </div>

+ 19 - 0
data/web/css/mailcow.css

@@ -117,3 +117,22 @@ legend {
 .lang-link-disabled  {
 	cursor: not-allowed;
 }
+.dkim-label {
+  margin: 0 0 2px !important;
+}
+.overlay {
+  background: #fff;
+  position: absolute;
+  z-index: 10000;
+  top: 0; right: 0; bottom: 0; left: 0;
+  opacity: 0.7;
+}
+nav .glyphicon {
+  font-size: 12px !important;
+}
+.logged-in-as {
+  border-left: 1px solid #E7E7E7;
+}
+#top {
+  padding-top: 70px;
+}

+ 26 - 1
data/web/edit.php

@@ -704,6 +704,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <input type="hidden" value="0" name="delete2duplicates">
             <input type="hidden" value="0" name="delete1">
             <input type="hidden" value="0" name="delete2">
+            <input type="hidden" value="0" name="automap">
+            <input type="hidden" value="0" name="skipcrossduplicates">
             <input type="hidden" value="0" name="active">
             <div class="form-group">
               <label class="control-label col-sm-2" for="host1"><?=$lang['edit']['hostname'];?></label>
@@ -743,6 +745,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               <label class="control-label col-sm-2" for="mins_interval"><?=$lang['edit']['mins_interval'];?></label>
               <div class="col-sm-10">
                 <input type="number" class="form-control" name="mins_interval" min="1" max="3600" value="<?=htmlspecialchars($result['mins_interval'], ENT_QUOTES, 'UTF-8');?>" required>
+                <small class="help-block">10-3600</small>
               </div>
             </div>
             <div class="form-group">
@@ -754,7 +757,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="maxage"><?=$lang['edit']['maxage'];?></label>
               <div class="col-sm-10">
-              <input type="number" class="form-control" name="maxage" id="maxage" value="<?=htmlspecialchars($result['maxage'], ENT_QUOTES, 'UTF-8');?>">
+              <input type="number" class="form-control" name="maxage" id="maxage" min="0" max="32000" value="<?=htmlspecialchars($result['maxage'], ENT_QUOTES, 'UTF-8');?>">
+              <small class="help-block">0-32000</small>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="maxbytespersecond"><?=$lang['edit']['maxbytespersecond'];?></label>
+              <div class="col-sm-10">
+              <input type="number" class="form-control" name="maxbytespersecond" id="maxbytespersecond" min="0" max="125000000" value="<?=htmlspecialchars($result['maxbytespersecond'], ENT_QUOTES, 'UTF-8');?>">
+              <small class="help-block">0-125000000</small>
               </div>
             </div>
             <div class="form-group">
@@ -784,6 +795,20 @@ if (isset($_SESSION['mailcow_cc_role'])) {
                 </div>
               </div>
             </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <div class="checkbox">
+                <label><input type="checkbox" value="1" name="automap" <?=($result['automap']=="1") ? "checked" : "";?>> <?=$lang['edit']['automap'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <div class="checkbox">
+                <label><input type="checkbox" value="1" name="skipcrossduplicates" <?=($result['skipcrossduplicates']=="1") ? "checked" : "";?>> <?=$lang['edit']['skipcrossduplicates'];?></label>
+                </div>
+              </div>
+            </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
                 <div class="checkbox">

+ 3 - 1
data/web/inc/footer.inc.php

@@ -22,7 +22,9 @@ function setLang(sel) {
   $.post( "<?= $_SERVER['REQUEST_URI']; ?>", {lang: sel} );
   window.location.href = window.location.pathname + window.location.search;
 }
-
+$(window).load(function() {
+  $(".overlay").hide();
+});
 $(document).ready(function() {
   window.mailcow_alert_box = function(message, type) {
     msg = $('<span/>').html(message).text();

+ 12 - 0
data/web/inc/functions.fail2ban.inc.php

@@ -12,6 +12,8 @@ function fail2ban($_action, $_data = null) {
         $data['ban_time'] = $redis->Get('F2B_BAN_TIME');
         $data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
         $data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
+        $data['netban_ipv4'] = $redis->Get('F2B_NETBAN_IPV4');
+        $data['netban_ipv6'] = $redis->Get('F2B_NETBAN_IPV6');
         $wl = $redis->hGetAll('F2B_WHITELIST');
         if (is_array($wl)) {
           foreach ($wl as $key => $value) {
@@ -50,6 +52,8 @@ function fail2ban($_action, $_data = null) {
         $ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
         $max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['active_int']);
         $retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
+        $netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
+        $netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
       }
       else {
         $_SESSION['return'] = array(
@@ -60,12 +64,20 @@ function fail2ban($_action, $_data = null) {
       }
       $wl = $_data['whitelist'];
       $ban_time = ($ban_time < 60) ? 60 : $ban_time;
+
+      $netban_ipv4 = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
+      $netban_ipv6 = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
+      $netban_ipv4 = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
+      $netban_ipv6 = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
+
       $max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
       $retry_window = ($retry_window < 1) ? 1 : $retry_window;
       try {
         $redis->Set('F2B_BAN_TIME', $ban_time);
         $redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
         $redis->Set('F2B_RETRY_WINDOW', $retry_window);
+        $redis->Set('F2B_NETBAN_IPV4', $netban_ipv4);
+        $redis->Set('F2B_NETBAN_IPV6', $netban_ipv6);
         $redis->Del('F2B_WHITELIST');
         if(!empty($wl)) {
           $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));

+ 9 - 2
data/web/inc/functions.inc.php

@@ -459,8 +459,9 @@ function user_get_alias_details($username) {
     while ($row = array_shift($run)) {
       $data['shared_aliases'] = $row['shared_aliases'];
     }
-    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') AS `direct_aliases` FROM `alias`
+    $stmt = $pdo->prepare("SELECT GROUP_CONCAT(`address` SEPARATOR ', ') AS `direct_aliases` FROM `alias`
       WHERE `goto` = :username_goto
+      AND `address` NOT LIKE '@%'
       AND `address` != :username_address");
     $stmt->execute(
       array(
@@ -477,7 +478,13 @@ function user_get_alias_details($username) {
     $stmt->execute(array(':username' => $username));
     $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
     while ($row = array_shift($run)) {
-      $data['ad_alias'] = $row['ad_alias'];
+      if (empty($data['direct_aliases'])) {
+        $data['direct_aliases'] = $row['ad_alias'];
+      }
+      else {
+        // Probably faster than imploding
+        $data['direct_aliases'] .= ', ' . $row['ad_alias'];
+      }
     }
     $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
     $stmt->execute(array(':username' => $username));

+ 50 - 16
data/web/inc/functions.mailbox.inc.php

@@ -213,24 +213,30 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
             return false;
           }
           $active  = intval($_data['active']);
-          $delete2duplicates = intval($_data['delete2duplicates']);
-          $delete1  = intval($_data['delete1']);
-          $delete2  = intval($_data['delete2']);
-          $port1            = $_data['port1'];
-          $host1            = strtolower($_data['host1']);
-          $password1        = $_data['password1'];
-          $exclude          = $_data['exclude'];
-          $maxage           = $_data['maxage'];
-          $subfolder2       = $_data['subfolder2'];
-          $user1            = $_data['user1'];
-          $mins_interval    = $_data['mins_interval'];
-          $enc1             = $_data['enc1'];
+          $delete2duplicates    = intval($_data['delete2duplicates']);
+          $delete1              = intval($_data['delete1']);
+          $delete2              = intval($_data['delete2']);
+          $skipcrossduplicates  = intval($_data['skipcrossduplicates']);
+          $automap              = intval($_data['automap']);
+          $port1                = $_data['port1'];
+          $host1                = strtolower($_data['host1']);
+          $password1            = $_data['password1'];
+          $exclude              = $_data['exclude'];
+          $maxage               = $_data['maxage'];
+          $maxbytespersecond    = $_data['maxbytespersecond'];
+          $subfolder2           = $_data['subfolder2'];
+          $user1                = $_data['user1'];
+          $mins_interval        = $_data['mins_interval'];
+          $enc1                = $_data['enc1'];
           if (empty($subfolder2)) {
             $subfolder2 = "";
           }
           if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32767)))) {
             $maxage = "0";
           }
+          if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
+            $maxbytespersecond = "0";
+          }
           if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
             $_SESSION['return'] = array(
               'type' => 'danger',
@@ -287,14 +293,17 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
             return false;
           }
           try {
-            $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`)
-              VALUES (:user2, :exclude, :delete1, :delete2, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)");
+            $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`)
+              VALUES (:user2, :exclude, :delete1, :delete2, :automap, :skipcrossduplicates, :maxbytespersecond, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)");
             $stmt->execute(array(
               ':user2' => $username,
               ':exclude' => $exclude,
               ':maxage' => $maxage,
               ':delete1' => $delete1,
               ':delete2' => $delete2,
+              ':automap' => $automap,
+              ':skipcrossduplicates' => $skipcrossduplicates,
+              ':maxbytespersecond' => $maxbytespersecond,
               ':subfolder2' => $subfolder2,
               ':host1' => $host1,
               ':authmech1' => 'PLAIN',
@@ -1444,6 +1453,8 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               $delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
               $delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
               $delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
+              $automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
+              $skipcrossduplicates = (isset($_data['skipcrossduplicates'])) ? intval($_data['skipcrossduplicates']) : $is_now['skipcrossduplicates'];
               $port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
               $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
               $host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
@@ -1452,6 +1463,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               $mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
               $exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude'];
               $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
+              $maxbytespersecond = (isset($_data['maxbytespersecond']) && $_data['maxbytespersecond'] != "") ? intval($_data['maxbytespersecond']) : $is_now['maxbytespersecond'];
             }
             else {
               $_SESSION['return'] = array(
@@ -1466,6 +1478,9 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
             if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32767)))) {
               $maxage = "0";
             }
+            if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
+              $maxbytespersecond = "0";
+            }
             if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
               $_SESSION['return'] = array(
                 'type' => 'danger',
@@ -1502,14 +1517,33 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               return false;
             }
             try {
-              $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, `delete2` = :delete2, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `last_run` = :last_run, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
-                WHERE `id` = :id");
+              $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1,
+                `delete2` = :delete2,
+                `automap` = :automap,
+                `skipcrossduplicates` = :skipcrossduplicates,
+                `maxage` = :maxage,
+                `maxbytespersecond` = :maxbytespersecond,
+                `subfolder2` = :subfolder2,
+                `exclude` = :exclude,
+                `host1` = :host1,
+                `last_run` = :last_run,
+                `user1` = :user1,
+                `password1` = :password1,
+                `mins_interval` = :mins_interval,
+                `port1` = :port1,
+                `enc1` = :enc1,
+                `delete2duplicates` = :delete2duplicates,
+                `active` = :active
+                  WHERE `id` = :id");
               $stmt->execute(array(
                 ':delete1' => $delete1,
                 ':delete2' => $delete2,
+                ':automap' => $automap,
+                ':skipcrossduplicates' => $skipcrossduplicates,
                 ':id' => $id,
                 ':exclude' => $exclude,
                 ':maxage' => $maxage,
+                ':maxbytespersecond' => $maxbytespersecond,
                 ':subfolder2' => $subfolder2,
                 ':host1' => $host1,
                 ':user1' => $user1,

+ 7 - 5
data/web/inc/header.inc.php

@@ -4,6 +4,7 @@
 <meta charset="utf-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1">
+<meta http-equiv="Referrer-Policy" content="same-origin">
 <title><?=$UI_TEXTS['title_name'];?></title>
 <!--[if lt IE 9]>
   <script src="/js/html5shiv.min.js"></script>
@@ -33,7 +34,8 @@
 <link rel="shortcut icon" href="/favicon.png" type="image/png">
 <link rel="icon" href="/favicon.png" type="image/png">
 </head>
-<body style="padding-top: 70px;" id="top">
+<body id="top">
+<div class="overlay"></div>
 <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
   <div class="container-fluid">
     <div class="navbar-header">
@@ -93,12 +95,12 @@
         <?php
         if (isset($_SESSION['mailcow_cc_role'])) {
         ?>
-        <li<?= (preg_match("/quarantaine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantaine.php"><span style="font-size: 12px;" class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantaine']; ?></a></li>
+        <li<?= (preg_match("/quarantaine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantaine.php"><span class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantaine']; ?></a></li>
         <?php
         }
         if ($_SESSION['mailcow_cc_role'] == 'admin') {
         ?>
-        <li><a href data-toggle="modal" data-container="sogo-mailcow" data-target="#RestartContainer"><span style="font-size: 12px;" class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_sogo']; ?></a></li>
+        <li><a href data-toggle="modal" data-container="sogo-mailcow" data-target="#RestartContainer"><span class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_sogo']; ?></a></li>
         <?php
         }
         ?>
@@ -126,11 +128,11 @@
         }
         if (!isset($_SESSION['dual-login']) && isset($_SESSION['mailcow_cc_username'])):
         ?>
-          <li><a href="#" style="border-left: 1px solid #E7E7E7;" onclick="logout.submit()"><?= sprintf($lang['header']['logged_in_as_logout'], $_SESSION['mailcow_cc_username']); ?></a></li>
+          <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b><?= $_SESSION['mailcow_cc_username']; ?></b> <span class="glyphicon glyphicon-log-out"></span></a></li>
         <?php
         elseif (isset($_SESSION['dual-login'])):
         ?>
-          <li><a href="#" style="border-left: 1px solid #E7E7E7;" onclick="logout.submit()"><?= sprintf($lang['header']['logged_in_as_logout_dual'], $_SESSION['mailcow_cc_username'], $_SESSION['dual-login']['username']); ?></a></li>
+          <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b><?= $_SESSION['mailcow_cc_username']; ?> <span class="text-info">(<?= $_SESSION['dual-login']['username']; ?>)</span> </b><span class="glyphicon glyphicon-log-out"></span></a></li>
         <?php
         endif;
         ?>

+ 9 - 5
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "20012021_2202";
+    $db_version = "27012018_1721";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -21,13 +21,14 @@ function init_db_schema() {
       AND active = '1'
       AND address NOT LIKE '@%'
       GROUP BY goto;",
-    "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as) AS
-      SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as FROM sender_acl
+    "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS
+      SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl
       WHERE send_as NOT LIKE '@%'
       GROUP BY logged_in_as;",
     "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
       SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
-      LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;",
+      LEFT OUTER JOIN alias_domain ON target_domain=domain
+      GROUP BY username;",
     "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
       SELECT md5(script_data), username, script_name, script_data FROM sieve_filters
       WHERE filter_type = 'prefilter';",
@@ -353,12 +354,15 @@ function init_db_schema() {
           "password1" => "VARCHAR(255) NOT NULL",
           "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''",
           "maxage" => "SMALLINT NOT NULL DEFAULT '0'",
-          "mins_interval" => "VARCHAR(50) NOT NULL",
+          "mins_interval" => "VARCHAR(50) NOT NULL DEFAULT '0'",
+          "maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'",
           "port1" => "SMALLINT NOT NULL",
           "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
           "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "automap" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "returned_text" => "TEXT",
           "last_run" => "TIMESTAMP NULL DEFAULT NULL",

+ 2 - 2
data/web/js/quarantaine.js

@@ -15,7 +15,7 @@ jQuery(function($){
         {"name":"sender","title":lang.sender,"breakpoints":"xs sm"},
         {"name":"rcpt","title":lang.rcpt, "type": "text"},
         {"name":"created","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.received,"style":{"width":"170px"}},
-        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right"},"style":{"width":"205px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right"},"style":{"width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
       ],
       "rows": $.ajax({
         dataType: 'json',
@@ -81,4 +81,4 @@ jQuery(function($){
   }
   // Initial table drawings
   draw_quarantaine_table();
-});
+});

+ 11 - 0
data/web/lang/lang.de.php

@@ -101,6 +101,7 @@ $lang['danger']['spam_alias_max_exceeded'] = 'Maximale Anzahl an Spam-Alias-Adre
 $lang['danger']['validity_missing'] = 'Bitte geben Sie eine Gültigkeitsdauer an';
 $lang['user']['loading'] = "Lade...";
 $lang['user']['active_sieve'] = "Aktiver Filter";
+$lang['user']['show_sieve_filters'] = "Zeige aktiven Filter des Benutzers";
 $lang['user']['no_active_filter'] = "Kein aktiver Filter vorhanden";
 $lang['user']['on'] = 'Ein';
 $lang['user']['off'] = 'Aus';
@@ -122,7 +123,9 @@ $lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse';
 $lang['user']['alias'] = 'Alias';
 $lang['user']['aliases'] = 'Aliasse';
 $lang['user']['shared_aliases'] = 'Geteilte Alias-Adressen';
+$lang['user']['shared_aliases_desc'] = 'Geteilte Alias-Adressen werden nicht bei benutzerdefinierten Einstellungen wie die des Spam-Filters oder der Verschlüsselungsrichtlinie berücksichtigt. Entsprechende Spam-Filter können lediglich von einem Administrator vorgenommen werden.';
 $lang['user']['direct_aliases'] = 'Direkte Alias-Adressen';
+$lang['user']['direct_aliases_desc'] = 'Nur direkte Alias-Adressen werden für benutzerdefinierte Einstellungen berücksichtigt.';
 $lang['user']['domain_aliases'] = 'Domain-Alias Adressen';
 $lang['user']['is_catch_all'] = 'Ist Catch-All Adresse für Domain(s)';
 $lang['user']['aliases_also_send_as'] = 'Darf außerdem versenden als Benutzer';
@@ -305,6 +308,12 @@ $lang['edit']['encryption'] = 'Verschlüsselung';
 $lang['edit']['maxage'] = 'Maximales Alter in Tagen einer Nachricht, die kopiert werden soll</br ><small>(0 = alle Nachrichten kopieren)</small>';
 $lang['edit']['subfolder2'] = 'Ziel-Ordner<br><small>(leer = kein Unterordner)</small>';
 $lang['edit']['mins_interval'] = 'Intervall (min)';
+$lang['edit']['maxbytespersecond'] = 'Max. Übertragungsrate in Bytes/s (0 für unlimitiert)';
+$lang['edit']['automap'] = 'Ordner automatisch mappen ("Sent items", "Sent" => "Sent" etc.)';
+$lang['edit']['skipcrossduplicates'] = 'Duplikate auch über Ordner hinweg überspringen ("first come, first serve")';
+$lang['add']['maxbytespersecond'] = 'Max. Übertragungsrate in Bytes/s (0 für unlimitiert)';
+$lang['add']['automap'] = 'Ordner automatisch mappen ("Sent items", "Sent" => "Sent" etc.)';
+$lang['add']['skipcrossduplicates'] = 'Duplikate auch über Ordner hinweg überspringen ("first come, first serve")';
 $lang['edit']['exclude'] = 'Elemente ausschließen (Regex)';
 $lang['edit']['archive'] = 'Archiv-Zugriff';
 $lang['edit']['max_mailboxes'] = 'Max. Mailboxanzahl:';
@@ -463,6 +472,8 @@ $lang['admin']['f2b_parameters'] = 'Fail2ban Parameter';
 $lang['admin']['f2b_ban_time'] = 'Banzeit (s)';
 $lang['admin']['f2b_max_attempts'] = 'Max. Versuche';
 $lang['admin']['f2b_retry_window'] = 'Wiederholungen im Zeitraum von (s)';
+$lang['admin']['f2b_netban_ipv4'] = 'Netzbereich für IPv4 Bans (8-32)';
+$lang['admin']['f2b_netban_ipv6'] = 'Netzbereich für IPv6 Bans (8-128)';
 $lang['admin']['f2b_whitelist'] = 'Whitelist für Netzwerke und Hosts';
 $lang['admin']['restrictions'] = 'Postfix Restriktionen';
 $lang['admin']['rr'] = 'Postfix Empfänger Restriktionen';

+ 11 - 0
data/web/lang/lang.en.php

@@ -101,6 +101,7 @@ $lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses
 $lang['danger']['validity_missing'] = 'Please assign a period of validity';
 $lang['user']['loading'] = "Loading...";
 $lang['user']['active_sieve'] = "Active filter";
+$lang['user']['show_sieve_filters'] = "Show active user sieve filter";
 $lang['user']['no_active_filter'] = "No active filter available";
 $lang['user']['on'] = "On";
 $lang['user']['off'] = "Off";
@@ -122,7 +123,9 @@ $lang['user']['spam_aliases'] = 'Temporary email aliases';
 $lang['user']['alias'] = 'Alias';
 $lang['user']['aliases'] = 'Aliases';
 $lang['user']['shared_aliases'] = 'Shared alias addresses';
+$lang['user']['shared_aliases_desc'] = 'A shared alias address is not affected by any user specific settings. A custom spam filter setting can be archived by a domain-wide policy set by an administrator..';
 $lang['user']['direct_aliases'] = 'Direct alias addresses';
+$lang['user']['direct_aliases_desc'] = 'Direct alias addresses are affected by spam filter and TLS policy settings.';
 $lang['user']['domain_aliases'] = 'Domain alias addresses';
 $lang['user']['is_catch_all'] = 'Catch-all for domain/s';
 $lang['user']['aliases_also_send_as'] = 'Also allowed to send as user';
@@ -303,6 +306,12 @@ $lang['edit']['username'] = 'Username';
 $lang['edit']['hostname'] = 'Hostname';
 $lang['edit']['encryption'] = 'Encryption';
 $lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote<br><small>(0 = ignore age)</small>';
+$lang['edit']['maxbytespersecond'] = 'Max. bytes per second (0 equals to unlimited)';
+$lang['edit']['automap'] = 'Try to automap folders ("Sent items", "Sent" => "Sent" etc.)';
+$lang['edit']['skipcrossduplicates'] = 'Skip duplicate messages across folders (first come, first serve)';
+$lang['add']['maxbytespersecond'] = 'Max. bytes per second (0 equals to unlimited)';
+$lang['add']['automap'] = 'Try to automap folders ("Sent items", "Sent" => "Sent" etc.)';
+$lang['add']['skipcrossduplicates'] = 'Skip duplicate messages across folders (first come, first serve)';
 $lang['edit']['subfolder2'] = 'Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>';
 $lang['edit']['mins_interval'] = 'Interval (min)';
 $lang['edit']['exclude'] = 'Exclude objects (regex)';
@@ -463,6 +472,8 @@ $lang['admin']['f2b_parameters'] = 'Fail2ban parameters';
 $lang['admin']['f2b_ban_time'] = 'Ban time (s)';
 $lang['admin']['f2b_max_attempts'] = 'Max. attempts';
 $lang['admin']['f2b_retry_window'] = 'Retry window (s) for max. attempts';
+$lang['admin']['f2b_netban_ipv4'] = 'IPv4 subnet size to apply ban on (8-32)';
+$lang['admin']['f2b_netban_ipv6'] = 'IPv6 subnet size to apply ban on (8-128)';
 $lang['admin']['f2b_whitelist'] = 'Whitelisted networks/hosts';
 $lang['admin']['search_domain_da'] = 'Search domains';
 $lang['admin']['restrictions'] = 'Postfix Restrictions';

+ 21 - 0
data/web/modals/mailbox.php

@@ -391,6 +391,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             <small class="help-block">0-32000</small>
 						</div>
 					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="maxbytespersecond"><?=$lang['edit']['maxbytespersecond'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="maxbytespersecond" id="maxbytespersecond" min="0" max="125000000" value="0">
+            <small class="help-block">0-125000000</small>
+						</div>
+					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="exclude"><?=$lang['add']['exclude'];?></label>
 						<div class="col-sm-10">
@@ -418,6 +425,20 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 							</div>
 						</div>
 					</div>
+          <div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" value="1" name="automap"> <?=$lang['add']['automap'];?></label>
+							</div>
+						</div>
+					</div>
+          <div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" value="1" name="skipcrossduplicates"> <?=$lang['add']['skipcrossduplicates'];?></label>
+							</div>
+						</div>
+					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">

+ 21 - 0
data/web/modals/user.php

@@ -70,6 +70,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             <small class="help-block">0-32000</small>
 						</div>
 					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="maxbytespersecond"><?=$lang['edit']['maxbytespersecond'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="maxbytespersecond" id="maxbytespersecond" min="0" max="125000000" value="0">
+            <small class="help-block">0-125000000</small>
+						</div>
+					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="exclude"><?=$lang['add']['exclude'];?></label>
 						<div class="col-sm-10">
@@ -97,6 +104,20 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 							</div>
 						</div>
 					</div>
+          <div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" value="1" name="automap"> <?=$lang['add']['automap'];?></label>
+							</div>
+						</div>
+					</div>
+          <div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" value="1" name="skipcrossduplicates"> <?=$lang['add']['skipcrossduplicates'];?></label>
+							</div>
+						</div>
+					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">

+ 7 - 9
data/web/user.php

@@ -97,7 +97,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   <div class="row">
     <div class="col-md-3 col-xs-5 text-right">  <span class="glyphicon glyphicon-filter"></span></div>
     <div class="col-md-9 col-xs-7">
-    <p><a href="#userFilterModal" data-toggle="modal">[Show active user sieve filter]</a></p>
+    <p><a href="#userFilterModal" data-toggle="modal">[<?=$lang['user']['show_sieve_filters'];?>]</a></p>
     </div>
   </div>
   <hr>
@@ -105,24 +105,22 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   $user_get_alias_details = user_get_alias_details($username);
   ?>
   <div class="row">
-    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['direct_aliases'];?>:</div>
+    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['direct_aliases'];?>:
+      <p class="small"><?=$lang['user']['direct_aliases_desc'];?></p>
+    </div>
     <div class="col-md-9 col-xs-7">
     <p><?=$user_get_alias_details['direct_aliases'];?></p>
     </div>
   </div>
   <div class="row">
-    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['shared_aliases'];?>:</div>
+    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['shared_aliases'];?>:
+      <p class="small"><?=$lang['user']['shared_aliases_desc'];?></p>
+    </div>
     <div class="col-md-9 col-xs-7">
     <p><?=$user_get_alias_details['shared_aliases'];?></p>
     </div>
   </div>
   <hr>
-  <div class="row">
-    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['domain_aliases'];?>:</div>
-    <div class="col-md-9 col-xs-7">
-    <p><?=$user_get_alias_details['ad_alias'];?></p>
-    </div>
-  </div>
   <div class="row">
     <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['aliases_also_send_as'];?>:</div>
     <div class="col-md-9 col-xs-7">

+ 33 - 30
docker-compose.yml

@@ -12,7 +12,7 @@ services:
       restart: always
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.254
+          ipv4_address: ${IPV4_NETWORK}.254
           aliases:
             - unbound
 
@@ -29,10 +29,9 @@ services:
         - MYSQL_PASSWORD=${DBPASS}
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.250
           aliases:
             - mysql
 
@@ -44,29 +43,31 @@ services:
       environment:
         - TZ=${TZ}
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.249
+          ipv4_address: ${IPV4_NETWORK}.249
           aliases:
             - redis
 
     clamd-mailcow:
-      image: mailcow/clamd:1.6
+      image: mailcow/clamd:1.8
       build: ./data/Dockerfiles/clamd
       restart: always
       environment:
-        - SKIP_CLAMD=${SKIP_CLAMD:-n}
         - TZ=${TZ}
+        - SKIP_CLAMD=${SKIP_CLAMD:-n}
+      volumes:
+        - ./data/conf/clamav/:/etc/clamav/
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
           aliases:
             - clamd
 
     rspamd-mailcow:
-      image: mailcow/rspamd:1.15
+      image: mailcow/rspamd:1.16
       build: ./data/Dockerfiles/rspamd
       stop_grace_period: 30s
       depends_on:
@@ -82,16 +83,15 @@ services:
         - rspamd-vol-1:/var/lib/rspamd
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       hostname: rspamd
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.253
           aliases:
             - rspamd
 
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.8
+      image: mailcow/phpfpm:1.9
       build: ./data/Dockerfiles/phpfpm
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
@@ -101,6 +101,9 @@ services:
         - ./data/conf/rspamd/dynmaps:/dynmaps:ro
         - dkim-vol-1:/data/dkim
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro
+        - ./data/conf/phpfpm/php-fpm.d/www.conf:/usr/local/etc/php-fpm.d/www.conf
+        - ./data/conf/phpfpm/php-fpm.d/system.conf:/usr/local/etc/php-fpm.d/system.conf
+        - ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini
       environment:
         - LOG_LINES=${LOG_LINES}
         - TZ=${TZ}
@@ -118,14 +121,14 @@ services:
         - SMTP_PORT=${SMTP_PORT:-25}
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
           aliases:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:1.13
+      image: mailcow/sogo:1.14
       build: ./data/Dockerfiles/sogo
       environment:
         - DBNAME=${DBNAME}
@@ -138,15 +141,14 @@ services:
         - ./data/conf/sogo/:/etc/sogo/
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.252
           aliases:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.17
+      image: mailcow/dovecot:1.18
       build: ./data/Dockerfiles/dovecot
       cap_add:
         - NET_BIND_SERVICE
@@ -176,7 +178,7 @@ services:
           soft: 20000
           hard: 40000
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       hostname: ${MAILCOW_HOSTNAME}
       networks:
         mailcow-network:
@@ -197,13 +199,15 @@ services:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
+      cap_add:
+        - NET_BIND_SERVICE
       ports:
         - "${SMTP_PORT:-25}:25"
         - "${SMTPS_PORT:-465}:465"
         - "${SUBMISSION_PORT:-587}:587"
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       hostname: ${MAILCOW_HOSTNAME}
       networks:
         mailcow-network:
@@ -214,7 +218,7 @@ services:
       image: memcached:alpine
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
           aliases:
@@ -249,10 +253,9 @@ services:
         - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
       restart: always
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.251
           aliases:
             - nginx
 
@@ -263,7 +266,7 @@ services:
       image: mailcow/acme:1.28
       build: ./data/Dockerfiles/acme
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       environment:
         - LOG_LINES=${LOG_LINES}
         - ADDITIONAL_SAN=${ADDITIONAL_SAN}
@@ -284,7 +287,7 @@ services:
             - acme
 
     fail2ban-mailcow:
-      image: mailcow/fail2ban:1.10
+      image: mailcow/fail2ban:1.11
       build: ./data/Dockerfiles/fail2ban
       stop_grace_period: 30s
       depends_on:
@@ -298,16 +301,17 @@ services:
       environment:
         - TZ=${TZ}
         - SKIP_FAIL2BAN=${SKIP_FAIL2BAN:-n}
+        - IPV4_NETWORK=${IPV4_NETWORK}
       network_mode: "host"
       dns:
-        - 172.22.1.254
+        - ${IPV4_NETWORK}.254
       volumes:
         - /lib/modules:/lib/modules:ro
 
     watchdog-mailcow:
-      image: mailcow/watchdog:1.12
+      image: mailcow/watchdog:1.13
       # Debug
-      #command: /watchdog.sh
+      command: /watchdog.sh
       build: ./data/Dockerfiles/watchdog
       volumes:
         - vmail-vol-1:/vmail:ro
@@ -323,7 +327,6 @@ services:
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
       networks:
         mailcow-network:
-          ipv4_address: 172.22.1.248
           aliases:
             - watchdog
 
@@ -358,8 +361,8 @@ networks:
     ipam:
       driver: default
       config:
-        - subnet: 172.22.1.0/24
-        - subnet: fd4d:6169:6c63:6f77::/64
+        - subnet: ${IPV4_NETWORK}.0/24
+        - subnet: ${IPV6_NETWORK}
 
 volumes:
   vmail-vol-1:

+ 8 - 0
generate_config.sh

@@ -101,8 +101,16 @@ USE_WATCHDOG=n
 # Send notifications by mail (no DKIM signature, sent from watchdog@MAILCOW_HOSTNAME)
 #WATCHDOG_NOTIFY_EMAIL=
 
+# Max log lines per service to keep in Redis logs
 LOG_LINES=9999
 
+# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)
+IPV4_NETWORK=172.22.1
+
+# Internal IPv6 subnet in fd00::/8
+IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
+
+
 EOF
 
 mkdir -p data/assets/ssl

+ 6 - 3
helper-scripts/nextcloud.sh

@@ -79,10 +79,12 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
 	  /web/nextcloud/occ config:system:set redis port --value=6379 --type=integer; \
 	  /web/nextcloud/occ config:system:set memcache.locking --value='\OC\Memcache\Redis' --type=string; \
 	  /web/nextcloud/occ config:system:set memcache.local --value='\OC\Memcache\Redis' --type=string; \
-	  /web/nextcloud/occ config:system:set trusted_proxies 0 --value=fd4d:6169:6c63:6f77::1; \
-	  /web/nextcloud/occ config:system:set trusted_proxies 1 --value=172.22.1.0/24; \
+	  /web/nextcloud/occ config:system:set trusted_domains 1 --value=${MAILCOW_HOSTNAME}; \
+    /web/nextcloud/occ config:system:set trusted_proxies 0 --value=${IPV6_NETWORK}; \
+	  /web/nextcloud/occ config:system:set trusted_proxies 1 --value=${IPV4_NETWORK}.0/24; \
 	  /web/nextcloud/occ config:system:set overwritewebroot --value=/nextcloud; \
 	  /web/nextcloud/occ config:system:set overwritehost --value=${MAILCOW_HOSTNAME}; \
+	  /web/nextcloud/occ config:system:set overwriteprotocol --value=https; \
 	  /web/nextcloud/occ config:system:set mail_smtpmode --value=smtp; \
 	  /web/nextcloud/occ config:system:set mail_smtpauthtype --value=LOGIN; \
 	  /web/nextcloud/occ config:system:set mail_from_address --value=nextcloud; \
@@ -94,10 +96,11 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
 	  /web/nextcloud/occ config:system:set user_backends 0 class --value=OC_User_IMAP"
 
 	if [[ ${NC_TYPE} == "subdomain" ]]; then
+		docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ config:system:set trusted_domains 1 --value=${NC_SUBD}
 		docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ config:system:set overwritewebroot --value=/
 		docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ config:system:set overwritehost --value=${NC_SUBD}
 		cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/
-		sed -i 's/NC_SUBD/${NC_SUBD}/g' ./data/conf/nginx/nextcloud.conf
+		sed -i "s/NC_SUBD/${NC_SUBD}/g" ./data/conf/nginx/nextcloud.conf
 	elif [[ ${NC_TYPE} == "subfolder" ]]; then
 		cp ./data/assets/nextcloud/site.nextcloud.custom ./data/conf/nginx/
 	fi

+ 124 - 106
update.sh

@@ -1,72 +1,91 @@
 #!/bin/bash
 
 for bin in curl docker-compose docker git awk sha1sum; do
-	if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
+  if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
 done
 
 [[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
 
-CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "USE_WATCHDOG" "WATCHDOG_NOTIFY_EMAIL" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN" "DOVEADM_PORT")
+CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "USE_WATCHDOG" "WATCHDOG_NOTIFY_EMAIL" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN" "DOVEADM_PORT" "IPV4_NETWORK" "IPV6_NETWORK" "LOG_LINES")
 sed -i '$a\' mailcow.conf
 for option in ${CONFIG_ARRAY[@]}; do
-	if [[ ${option} == "ADDITIONAL_SAN" ]]; then
-		if ! grep -q ${option} mailcow.conf; then
-			echo "Adding new option \"${option}\" to mailcow.conf"
-			echo "${option}=" >> mailcow.conf
-		fi
-	elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then
-		if ! grep -q ${option} mailcow.conf; then
-			echo "Adding new option \"${option}\" to mailcow.conf"
-			echo "COMPOSE_PROJECT_NAME=mailcow-dockerized" >> mailcow.conf
-		fi
-	elif [[ ${option} == "DOVEADM_PORT" ]]; then
-		if ! grep -q ${option} mailcow.conf; then
-			echo "Adding new option \"${option}\" to mailcow.conf"
-			echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
-		fi
-	elif [[ ${option} == "WATCHDOG_NOTIFY_EMAIL" ]]; then
-		if ! grep -q ${option} mailcow.conf; then
-			echo "Adding new option \"${option}\" to mailcow.conf"
-			echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf
-		fi
+  if [[ ${option} == "ADDITIONAL_SAN" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo "${option}=" >> mailcow.conf
+    fi
+  elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo "COMPOSE_PROJECT_NAME=mailcow-dockerized" >> mailcow.conf
+    fi
+  elif [[ ${option} == "DOVEADM_PORT" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
+    fi
+  elif [[ ${option} == "WATCHDOG_NOTIFY_EMAIL" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf
+    fi
   elif [[ ${option} == "LOG_LINES" ]]; then
     if ! grep -q ${option} mailcow.conf; then
       echo "Adding new option \"${option}\" to mailcow.conf"
+      echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
       echo "LOG_LINES=9999" >> mailcow.conf
     fi
-	elif ! grep -q ${option} mailcow.conf; then
-		echo "Adding new option \"${option}\" to mailcow.conf"
-		echo "${option}=n" >> mailcow.conf
-	fi
+  elif [[ ${option} == "IPV4_NETWORK" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
+      echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
+    fi
+  elif [[ ${option} == "IPV6_NETWORK" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo '# Internal IPv6 subnet in fd00::/8' >> mailcow.conf
+      echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf
+    fi
+  elif ! grep -q ${option} mailcow.conf; then
+    echo "Adding new option \"${option}\" to mailcow.conf"
+    echo "${option}=n" >> mailcow.conf
+  fi
 done
 
 echo -en "Checking internet connection... "
 curl -o /dev/null google.com -sm3
 if [[ $? != 0 ]]; then
-	echo -e "\e[31mfailed\e[0m"
-	exit 1
-else
-	echo -e "\e[32mOK\e[0m"
+  echo -e "\e[31mfailed\e[0m"
+  exit 1
+  else
+  echo -e "\e[32mOK\e[0m"
 fi
 
 set -o pipefail
 export LC_ALL=C
 DATE=$(date +%Y-%m-%d_%H_%M_%S)
 BRANCH=$(git rev-parse --abbrev-ref HEAD)
-
-case "${1}" in
-	--check|-c)
-		echo "Checking remote code for updates..."
-		git fetch origin ${BRANCH}
-		if ! git diff origin/${BRANCH} --quiet; then
-			echo "Updated code is available."
-			exit 0
-		else
-			echo "No updates available."
-			exit 3
-		fi
-	;;
-esac
+declare -a DC_PARAMS
+
+while (($#)); do
+  case "${1}" in
+    --check|-c)
+      echo "Checking remote code for updates..."
+      git fetch origin ${BRANCH}
+      if [[ $(git branch ${BRANCH} --contains $(git rev-parse origin/${BRANCH}) > /dev/null 2> /dev/null; echo $?) != 0 ]]; then
+        echo "Updated code is available."
+        exit 0
+      else
+        echo "No updates available."
+        exit 3
+      fi
+    ;;
+    --no-start)
+      DC_PARAMS=(${DC_PARAMS[@]} "--no-start")
+    ;;
+  esac
+done
 
 echo -e "\e[32mChecking for newer update script...\e[0m"
 SHA1_1=$(sha1sum update.sh)
@@ -74,22 +93,22 @@ git fetch origin ${BRANCH}
 git checkout origin/${BRANCH} update.sh
 SHA1_2=$(sha1sum update.sh)
 if [[ ${SHA1_1} != ${SHA1_2} ]]; then
-	echo "update.sh changed, please run this script again, exiting."
-	chmod +x update.sh
-	exit 0
+  echo "update.sh changed, please run this script again, exiting."
+  chmod +x update.sh
+  exit 0
 fi
 
 if [[ -f mailcow.conf ]]; then
-	source mailcow.conf
-else
-	echo -e "\e[31mNo mailcow.conf - is mailcow installed?\e[0m"
-	exit 1
+  source mailcow.conf
+  else
+  echo -e "\e[31mNo mailcow.conf - is mailcow installed?\e[0m"
+  exit 1
 fi
 
 read -r -p "Are you sure you want to update mailcow: dockerized? All containers will be stopped. [y/N] " response
 if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-	echo "OK, exiting."
-	exit 0
+  echo "OK, exiting."
+  exit 0
 fi
 
 echo -e "Stopping mailcow... "
@@ -109,31 +128,31 @@ git merge -Xtheirs -Xpatience -m "After update on ${DATE}"
 # Need to use a variable to not pass return codes of if checks
 MERGE_RETURN=$?
 if [[ ${MERGE_RETURN} == 128 ]]; then
-	echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m"
-	exit 1
+  echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m"
+  exit 1
 elif [[ ${MERGE_RETURN} == 1 ]]; then
-	echo -e "\e[93mPotenial conflict, trying to fix...\e[0m"
-	git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v
-	git add -A
-	git commit -m "After update on ${DATE}" > /dev/null
-	git checkout .
-	echo -e "\e[32mRemoved and recreated files if necessary.\e[0m"
+  echo -e "\e[93mPotenial conflict, trying to fix...\e[0m"
+  git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v
+  git add -A
+  git commit -m "After update on ${DATE}" > /dev/null
+  git checkout .
+  echo -e "\e[32mRemoved and recreated files if necessary.\e[0m"
 elif [[ ${MERGE_RETURN} != 0 ]]; then
-	echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
-	echo
-	echo "Run docker-compose up -d to restart your stack without updates or try again after fixing the mentioned errors."
-	exit 1
+  echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
+  echo
+  echo "Run docker-compose up -d to restart your stack without updates or try again after fixing the mentioned errors."
+  exit 1
 fi
 
 
 echo -e "\e[32mFetching new docker-compose version...\e[0m"
 sleep 2
 if [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php -o /dev/null) == "200" ]]; then
-	LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
-	curl -#L https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $(which docker-compose)
-	chmod +x $(which docker-compose)
-else
-	echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m"
+  LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
+  curl -#L https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $(which docker-compose)
+  chmod +x $(which docker-compose)
+  else
+  echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m"
 fi
 
 echo -e "\e[32mFetching new images, if any...\e[0m"
@@ -146,48 +165,47 @@ cp -n data/assets/ssl-example/*.pem data/assets/ssl/
 
 echo -e "\e[32mStarting mailcow...\e[0m"
 sleep 2
-docker-compose up -d --remove-orphans
+docker-compose up -d --remove-orphans ${DC_PARAMS[@]}
 
 echo -e "\e[32mCollecting garbage...\e[0m"
 IMGS_TO_DELETE=()
 for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do
-	REPOSITORY=${container/:*}
-	TAG=${container/*:}
-	V_MAIN=${container/*.}
-	V_SUB=${container/*.}
-
-	EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')
-	for existing_tag in ${EXISTING_TAGS[@]}; do
-		V_MAIN_EXISTING=${existing_tag/*.}
-		V_SUB_EXISTING=${existing_tag/*.}
-
-		# Not an integer
-		[[ ! $V_MAIN_EXISTING =~ ^[0-9]+$ ]] && continue
-		[[ ! $V_SUB_EXISTING =~ ^[0-9]+$ ]] && continue
-
-		if [[ $V_MAIN_EXISTING == "latest" ]]; then
-			echo "Found deprecated label \"latest\" for repository $REPOSITORY, it should be deleted."
-			IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
-		elif [[ $V_MAIN_EXISTING -lt $V_MAIN ]]; then
-			echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
-			IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
-		elif [[ $V_SUB_EXISTING -lt $V_SUB ]]; then
-			echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
-			IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
-		fi
-	done
+  REPOSITORY=${container/:*}
+  TAG=${container/*:}
+  V_MAIN=${container/*.}
+  V_SUB=${container/*.}
+  EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')
+  for existing_tag in ${EXISTING_TAGS[@]}; do
+    V_MAIN_EXISTING=${existing_tag/*.}
+    V_SUB_EXISTING=${existing_tag/*.}
+    # Not an integer
+    [[ ! $V_MAIN_EXISTING =~ ^[0-9]+$ ]] && continue
+    [[ ! $V_SUB_EXISTING =~ ^[0-9]+$ ]] && continue
+
+    if [[ $V_MAIN_EXISTING == "latest" ]]; then
+      echo "Found deprecated label \"latest\" for repository $REPOSITORY, it should be deleted."
+      IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
+    elif [[ $V_MAIN_EXISTING -lt $V_MAIN ]]; then
+      echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
+      IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
+    elif [[ $V_SUB_EXISTING -lt $V_SUB ]]; then
+      echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
+      IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
+    fi
+  done
 done
+
 if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
-	echo "Run the following command to delete unused image tags:"
-	echo
-	echo "    docker rmi ${IMGS_TO_DELETE[*]}"
-	echo
-	read -r -p "Do you want to delete old image tags right now? [y/N] " response
-	if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-		docker rmi ${IMGS_TO_DELETE[*]}
-	else
-		echo "OK, skipped."
-	fi
+  echo "Run the following command to delete unused image tags:"
+  echo
+  echo "    docker rmi ${IMGS_TO_DELETE[*]}"
+  echo
+  read -r -p "Do you want to delete old image tags right now? [y/N] " response
+  if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+    docker rmi ${IMGS_TO_DELETE[*]}
+    else
+    echo "OK, skipped."
+  fi
 fi
 echo -e "\e[32mFurther cleanup...\e[0m"
 echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\""