2
0
Эх сурвалжийг харах

Merge pull request #963 from mailcow/dev

Dev
André Peters 7 жил өмнө
parent
commit
55586f4cb1
41 өөрчлөгдсөн 706 нэмэгдсэн , 352 устгасан
  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. 10 6
      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. 27 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. 4 0
      data/web/js/fontawesome.min.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. 34 31
      docker-compose.yml
  40. 6 3
      helper-scripts/nextcloud.sh
  41. 123 105
      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} ))

+ 10 - 6
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/;
 
@@ -48,8 +48,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;
 
@@ -112,7 +114,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/;
 
@@ -135,8 +137,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;
 

+ 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 = [::]:9001
+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;
+}

+ 27 - 1
data/web/edit.php

@@ -661,12 +661,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         $id = $_GET["syncjob"];
         $result = mailbox('get', 'syncjob_details', $id);
         if (!empty($result)) {
+          print_r($result);
         ?>
           <h4><?=$lang['edit']['syncjob'];?></h4>
           <form class="form-horizontal" data-id="editsyncjob" role="form" method="post">
             <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>
@@ -706,6 +709,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">
@@ -717,7 +721,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">
@@ -747,6 +759,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 = "02012018_1515";
+    $db_version = "24012018_1219";
 
     $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",

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 4 - 0
data/web/js/fontawesome.min.js


+ 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">

+ 34 - 31
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:
@@ -184,7 +186,7 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: mailcow/postfix:1.11
+      image: mailcow/postfix:1.13
       build: ./data/Dockerfiles/postfix
       volumes:
         - ./data/conf/postfix:/opt/postfix/conf
@@ -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:

+ 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

+ 123 - 105
update.sh

@@ -1,7 +1,7 @@
 #!/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;}
@@ -9,64 +9,83 @@ done
 CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "USE_WATCHDOG" "WATCHDOG_NOTIFY_EMAIL" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN" "DOVEADM_PORT")
 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\""

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно