Răsfoiți Sursa

Merge pull request #1 from mailcow/master

update to latest master
Timo N 7 ani în urmă
părinte
comite
52085cc3d5
90 a modificat fișierele cu 2744 adăugiri și 2316 ștergeri
  1. 3 0
      .gitignore
  2. 5 5
      data/Dockerfiles/acme/docker-entrypoint.sh
  3. 5 3
      data/Dockerfiles/dockerapi/Dockerfile
  4. 120 13
      data/Dockerfiles/dockerapi/server.py
  5. 11 5
      data/Dockerfiles/dovecot/Dockerfile
  6. 16 10
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  7. 2 0
      data/Dockerfiles/dovecot/maildir_gc.sh
  8. 0 1
      data/Dockerfiles/dovecot/postlogin.sh
  9. 2 2
      data/Dockerfiles/dovecot/rspamd-pipe-ham
  10. 2 2
      data/Dockerfiles/dovecot/rspamd-pipe-spam
  11. 25 0
      data/Dockerfiles/dovecot/sa-rules.sh
  12. 6 7
      data/Dockerfiles/dovecot/trim_logs.sh
  13. 2 2
      data/Dockerfiles/phpfpm/Dockerfile
  14. 6 4
      data/Dockerfiles/phpfpm/docker-entrypoint.sh
  15. 23 15
      data/Dockerfiles/postfix/postfix.sh
  16. 2 2
      data/Dockerfiles/postfix/rspamd-pipe-ham
  17. 2 2
      data/Dockerfiles/postfix/rspamd-pipe-spam
  18. 1 1
      data/Dockerfiles/rspamd/Dockerfile
  19. 4 1
      data/Dockerfiles/rspamd/docker-entrypoint.sh
  20. 0 152
      data/Dockerfiles/rspamd/lua_util.lua
  21. 0 674
      data/Dockerfiles/rspamd/ratelimit.lua
  22. 2 0
      data/Dockerfiles/sogo/Dockerfile
  23. 19 19
      data/Dockerfiles/sogo/bootstrap-sogo.sh
  24. 0 7
      data/Dockerfiles/sogo/supervisord.conf
  25. 1 0
      data/Dockerfiles/unbound/Dockerfile
  26. 4 1
      data/Dockerfiles/unbound/docker-entrypoint.sh
  27. 23 19
      data/Dockerfiles/watchdog/watchdog.sh
  28. 192 0
      data/assets/mysql/docker-entrypoint.sh
  29. 11 8
      data/conf/dovecot/dovecot.conf
  30. 20 33
      data/conf/nginx/site.conf
  31. 1 1
      data/conf/phpfpm/php-fpm.d/pools.conf
  32. 3 1
      data/conf/postfix/main.cf
  33. 10 7
      data/conf/rspamd/custom/bad_asn.map
  34. 2 1
      data/conf/rspamd/dynmaps/settings.php
  35. 0 15
      data/conf/rspamd/local.d/multimap.conf
  36. 62 0
      data/conf/rspamd/lua/rspamd.local.lua
  37. 2 1
      data/conf/rspamd/meta_exporter/pipe.php
  38. 2 2
      data/conf/rspamd/override.d/worker-controller.inc
  39. 6 6
      data/conf/sogo/sogo.conf
  40. 10 1
      data/conf/unbound/unbound.conf
  41. 61 62
      data/web/admin.php
  42. 2 1
      data/web/autodiscover.php
  43. 0 6
      data/web/css/admin.css
  44. 0 3
      data/web/css/mailbox.css
  45. 10 0
      data/web/css/mailcow.css
  46. 2 2
      data/web/debug.php
  47. 169 58
      data/web/edit.php
  48. 153 163
      data/web/img/cow_mailcow.svg
  49. 17 104
      data/web/inc/footer.inc.php
  50. 216 0
      data/web/inc/functions.acl.inc.php
  51. 76 130
      data/web/inc/functions.address_rewriting.inc.php
  52. 18 15
      data/web/inc/functions.docker.inc.php
  53. 67 147
      data/web/inc/functions.domain_admin.inc.php
  54. 37 61
      data/web/inc/functions.inc.php
  55. 63 35
      data/web/inc/functions.mailbox.inc.php
  56. 16 16
      data/web/inc/functions.policy.inc.php
  57. 11 12
      data/web/inc/functions.quarantine.inc.php
  58. 8 0
      data/web/inc/functions.ratelimit.inc.php
  59. 157 0
      data/web/inc/functions.tls_policy_maps.inc.php
  60. 125 125
      data/web/inc/header.inc.php
  61. 49 5
      data/web/inc/init_db.inc.php
  62. 16 3
      data/web/inc/prerequisites.inc.php
  63. 1 1
      data/web/inc/triggers.inc.php
  64. 10 0
      data/web/inc/vars.inc.php
  65. 4 4
      data/web/index.php
  66. 13 9
      data/web/js/admin.js
  67. 6 5
      data/web/js/api.js
  68. 25 19
      data/web/js/edit.js
  69. 108 31
      data/web/js/mailbox.js
  70. 193 0
      data/web/js/mailcow.js
  71. 1 1
      data/web/js/quarantine.js
  72. 1 0
      data/web/js/sha1.min.js
  73. 3 3
      data/web/js/user.js
  74. 41 1
      data/web/json_api.php
  75. 44 8
      data/web/lang/lang.de.php
  76. 41 4
      data/web/lang/lang.en.php
  77. 1 1
      data/web/lang/lang.nl.php
  78. 70 44
      data/web/mailbox.php
  79. 10 10
      data/web/modals/admin.php
  80. 13 13
      data/web/modals/footer.php
  81. 108 53
      data/web/modals/mailbox.php
  82. 15 15
      data/web/modals/user.php
  83. 6 6
      data/web/quarantine.php
  84. 50 93
      data/web/user.php
  85. 30 19
      docker-compose.yml
  86. 6 0
      generate_config.sh
  87. 21 3
      helper-scripts/backup_and_restore.sh
  88. 31 0
      helper-scripts/ext_sql_sock.docker-compose.override.yml
  89. 1 1
      helper-scripts/nextcloud.sh
  90. 11 1
      update.sh

+ 3 - 0
.gitignore

@@ -21,3 +21,6 @@ data/conf/nginx/*.conf
 data/conf/nginx/*.custom
 data/conf/nginx/*.bak
 data/conf/dovecot/extra.conf
+data/conf/rspamd/custom/*
+data/conf/portainer/
+docker-compose.override.yml

+ 5 - 5
data/Dockerfiles/acme/docker-entrypoint.sh

@@ -37,7 +37,7 @@ mkdir -p ${ACME_BASE}/acme/private
 restart_containers(){
   for container in $*; do
     log_f "Restarting ${container}..." no_nl
-    C_REST_OUT=$(curl -X POST http://dockerapi:8080/containers/${container}/restart | jq -r '.msg')
+    C_REST_OUT=$(curl -X POST --insecure https://dockerapi/containers/${container}/restart | jq -r '.msg')
     log_f "${C_REST_OUT}" no_date
   done
 }
@@ -125,7 +125,7 @@ else
 fi
 
 log_f "Waiting for database... "
-while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do
+while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
   sleep 2
 done
 log_f "Initializing, please wait... "
@@ -161,19 +161,19 @@ while true; do
   fi
 
   # Container ids may have changed
-  CONTAINERS_RESTART=($(curl --silent http://dockerapi:8080/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("nginx-mailcow") or contains("postfix-mailcow") or contains("dovecot-mailcow")) | .id' | tr "\n" " "))
+  CONTAINERS_RESTART=($(curl --silent --insecure https://dockerapi/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("nginx-mailcow") or contains("postfix-mailcow") or contains("dovecot-mailcow")) | .id' | tr "\n" " "))
 
   log_f "Waiting for domain table... " no_nl
   while [[ -z ${DOMAIN_TABLE} ]]; do
     curl --silent http://nginx/ >/dev/null 2>&1
-    DOMAIN_TABLE=$(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
+    DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
     [[ -z ${DOMAIN_TABLE} ]] && sleep 10
   done
   log_f "OK" no_date
 
   while read domains; do
     SQL_DOMAIN_ARR+=("${domains}")
-  done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 UNION SELECT alias_domain FROM alias_domain" -Bs)
+  done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 UNION SELECT alias_domain FROM alias_domain" -Bs)
 
   for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
     A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1)

+ 5 - 3
data/Dockerfiles/dockerapi/Dockerfile

@@ -1,8 +1,10 @@
-FROM python:2-alpine
+FROM alpine:3.8
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
-RUN apk add -U --no-cache iptables ip6tables tzdata
-RUN pip install docker==3.0.1 flask flask-restful
+RUN apk add -U --no-cache python2 python-dev py-pip gcc musl-dev tzdata openssl-dev libffi-dev \
+  && pip2 install --upgrade docker==3.0.1 flask flask-restful pyOpenSSL \
+  && apk del python-dev py2-pip gcc
 
 COPY server.py /
+
 CMD ["python2", "-u", "/server.py"]

+ 120 - 13
data/Dockerfiles/dockerapi/server.py

@@ -3,12 +3,16 @@ from flask_restful import Resource, Api
 from flask import jsonify
 from flask import request
 from threading import Thread
+from OpenSSL import crypto
 import docker
+import uuid
 import signal
 import time
 import os
 import re
 import sys
+import ssl
+import socket
 
 docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
 app = Flask(__name__)
@@ -93,22 +97,74 @@ class container_post(Resource):
               return sieve_return.output
           except Exception as e:
             return jsonify(type='danger', msg=str(e))
+        # not in use...
+        elif request.json['cmd'] == 'mail_crypt_generate' and request.json['username'] and request.json['old_password'] and request.json['new_password']:
+          try:
+            for container in docker_client.containers.list(filters={"id": container_id}):
+              # create if missing
+              crypto_generate = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm mailbox cryptokey generate -u '" + request.json['username'].replace("'", "'\\''") + "' -URf"], user='vmail')
+              if crypto_generate.exit_code == 0:
+                # open a shell, bind stdin and return socket
+                cryptokey_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='vmail')
+                # command to be piped to shell
+                cryptokey_cmd = "/usr/local/bin/doveadm mailbox cryptokey password -u '" + request.json['username'].replace("'", "'\\''") + "' -n '" + request.json['new_password'].replace("'", "'\\''") + "' -o '" + request.json['old_password'].replace("'", "'\\''") + "'\n"
+                # socket is .output
+                cryptokey_socket = cryptokey_shell.output;
+                try :
+                  # send command utf-8 encoded
+                  cryptokey_socket.sendall(cryptokey_cmd.encode('utf-8'))
+                  # we won't send more data than this
+                  cryptokey_socket.shutdown(socket.SHUT_WR)
+                except socket.error:
+                  # exit on socket error
+                  return jsonify(type='danger', msg=str('socket error'))
+                # read response
+                cryptokey_response = recv_socket_data(cryptokey_socket)
+                crypto_error = re.search('dcrypt_key_load_private.+failed.+error', cryptokey_response)
+                if crypto_error is not None:
+                  return jsonify(type='danger', msg=str("dcrypt_key_load_private error"))
+                return jsonify(type='success', msg=str("key pair generated"))
+              else:
+                return jsonify(type='danger', msg=str(crypto_generate.output))
+          except Exception as e:
+            return jsonify(type='danger', msg=str(e))
+        elif request.json['cmd'] == 'maildir_cleanup' and request.json['maildir']:
+          try:
+            for container in docker_client.containers.list(filters={"id": container_id}):
+              sane_name = re.sub(r'\W+', '', request.json['maildir'])
+              maildir_cleanup = container.exec_run(["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"], user='vmail')
+              if maildir_cleanup.exit_code == 0:
+                return jsonify(type='success', msg=str("moved to garbage"))
+              else:
+                return jsonify(type='danger', msg=str(maildir_cleanup.output))
+          except Exception as e:
+            return jsonify(type='danger', msg=str(e))
         elif request.json['cmd'] == 'worker_password' and request.json['raw']:
           try:
             for container in docker_client.containers.list(filters={"id": container_id}):
-              hash = container.exec_run(["/bin/bash", "-c", "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null"], user='_rspamd')
-              if hash.exit_code == 0:
-                hash_stdout = str(hash.output)
-                for line in hash_stdout.split("\n"):
-                  if '$2$' in line:
-                    hash = line.strip()
-                f = open("/access.inc", "w")
-                f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash.rstrip()) + '";\n')
-                f.close()
-                container.restart()
+              worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd')
+              worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n"
+              worker_socket = worker_shell.output;
+              try :
+                worker_socket.sendall(worker_cmd.encode('utf-8'))
+                worker_socket.shutdown(socket.SHUT_WR)
+              except socket.error:
+                return jsonify(type='danger', msg=str('socket error'))
+              worker_response = recv_socket_data(worker_socket)
+              matched = False
+              for line in worker_response.split("\n"):
+                if '$2$' in line:
+                  matched = True
+                  hash = line.strip()
+                  hash_out = re.search('\$2\$.+$', hash).group(0)
+                  f = open("/access.inc", "w")
+                  f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n')
+                  f.close()
+                  container.restart()
+              if matched:
                 return jsonify(type='success', msg='command completed successfully')
               else:
-                return jsonify(type='danger', msg='command did not complete, exit code was ' + int(hash.exit_code))
+                return jsonify(type='danger', msg='command did not complete')
           except Exception as e:
             return jsonify(type='danger', msg=str(e))
         elif request.json['cmd'] == 'mailman_password' and request.json['email'] and request.json['passwd']:
@@ -137,11 +193,62 @@ class GracefulKiller:
     signal.signal(signal.SIGINT, self.exit_gracefully)
     signal.signal(signal.SIGTERM, self.exit_gracefully)
 
-  def exit_gracefully(self,signum, frame):
+  def exit_gracefully(self, signum, frame):
     self.kill_now = True
 
 def startFlaskAPI():
-  app.run(debug=False, host='0.0.0.0', port=8080, threaded=True)
+  create_self_signed_cert()
+  try:
+    ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+    ctx.check_hostname = False
+    ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem')
+  except:
+    print "Cannot initialize TLS, retrying in 5s..."
+    time.sleep(5)
+  app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
+
+def recv_socket_data(c_socket, timeout=10):
+  c_socket.setblocking(0)
+  total_data=[];
+  data='';
+  begin=time.time()
+  while True:
+    if total_data and time.time()-begin > timeout:
+      break
+    elif time.time()-begin > timeout*2:
+      break
+    try:
+      data = c_socket.recv(8192)
+      if data:
+        total_data.append(data)
+        #change the beginning time for measurement
+        begin=time.time()
+      else:
+        #sleep for sometime to indicate a gap
+        time.sleep(0.1)
+        break
+    except:
+      pass
+  return ''.join(total_data)
+
+def create_self_signed_cert():
+  pkey = crypto.PKey()
+  pkey.generate_key(crypto.TYPE_RSA, 2048)
+  cert = crypto.X509()
+  cert.get_subject().O = "mailcow"
+  cert.get_subject().CN = "dockerapi"
+  cert.set_serial_number(int(uuid.uuid4()))
+  cert.gmtime_adj_notBefore(0)
+  cert.gmtime_adj_notAfter(10*365*24*60*60)
+  cert.set_issuer(cert.get_subject())
+  cert.set_pubkey(pkey)
+  cert.sign(pkey, 'sha512')
+  cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
+  pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
+  with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle:
+    handle.write(cert)
+  with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
+    handle.write(pkey)
 
 api.add_resource(containers_get, '/containers/json')
 api.add_resource(container_get, '/containers/<string:container_id>/json')

+ 11 - 5
data/Dockerfiles/dovecot/Dockerfile

@@ -14,6 +14,7 @@ RUN apt-get update && apt-get -y --no-install-recommends install \
   cpanminus \
   curl \
   default-libmysqlclient-dev \
+  dnsutils \
   libjson-webtoken-perl \
   libcgi-pm-perl \
   libcrypt-openssl-rsa-perl \
@@ -88,10 +89,11 @@ RUN curl https://www.dovecot.org/releases/2.3/dovecot-$DOVECOT_VERSION.tar.gz |
   && rm -rf dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION
 
 RUN cpanm Data::Uniqid Mail::IMAPClient String::Util
-RUN echo '* * * * *   root   /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync
-RUN echo '30 3 * * *   vmail  /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync
-RUN echo '* * * * *   root  /usr/local/bin/trim_logs.sh >> /dev/stdout 2>&1' > /etc/cron.d/trim_logs
-
+RUN echo '* * * * *   root  /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync
+RUN echo '30 3 * * *  vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync
+RUN echo '* * * * *   vmail /usr/local/bin/trim_logs.sh >> /dev/stdout 2>&1' > /etc/cron.d/trim_logs
+RUN echo '25 * * * *  vmail /usr/local/bin/maildir_gc.sh >> /dev/stdout 2>&1' > /etc/cron.d/maildir_gc
+RUN echo '30 1 * * *  root  /usr/local/bin/sa-rules.sh  >> /dev/stdout 2>&1' > /etc/cron.d/sa-rules
 COPY trim_logs.sh /usr/local/bin/trim_logs.sh
 COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
 COPY imapsync /usr/local/bin/imapsync
@@ -101,6 +103,8 @@ COPY report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve
 COPY report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve
 COPY rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham
 COPY rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam
+COPY sa-rules.sh /usr/local/bin/sa-rules.sh
+COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh
 COPY docker-entrypoint.sh /
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 
@@ -109,7 +113,9 @@ RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \
   /usr/local/bin/imapsync_cron.pl \
   /usr/local/bin/postlogin.sh \
   /usr/local/bin/imapsync \
-  /usr/local/bin/trim_logs.sh
+  /usr/local/bin/trim_logs.sh \
+  /usr/local/bin/sa-rules.sh \
+  /usr/local/bin/maildir_gc.sh
 
 RUN groupadd -g 5000 vmail \
   && groupadd -g 401 dovecot \

+ 16 - 10
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -2,7 +2,7 @@
 set -e
 
 # Wait for MySQL to warm-up
-while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do
+while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
   echo "Waiting for database to come up..."
   sleep 2
 done
@@ -15,6 +15,7 @@ sed -i "s/LOG_LINES/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh
 
 # Create missing directories
 [[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/
+[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
 [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
 [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
 
@@ -23,7 +24,7 @@ DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 
 # Create quota dict for Dovecot
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf
-connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 map {
   pattern = priv/quota/storage
   table = quota2
@@ -40,7 +41,7 @@ EOF
 
 # Create dict used for sieve pre and postfilters
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
-connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 map {
   pattern = priv/sieve/name/\$script_name
   table = sieve_before
@@ -62,7 +63,7 @@ map {
 EOF
 
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
-connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 map {
   pattern = priv/sieve/name/\$script_name
   table = sieve_after
@@ -87,7 +88,7 @@ EOF
 # Create userdb dict for Dovecot
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf
 driver = mysql
-connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1'
 iterate_query = SELECT username FROM mailbox WHERE active='1';
 EOF
@@ -95,7 +96,7 @@ EOF
 # Create pass dict for Dovecot
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf
 driver = mysql
-connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS} ssl_verify_server_cert=no ssl_ca=/etc/ssl/certs/ca-certificates.crt"
 default_pass_scheme = SSHA256
 password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_EXTRACT(attributes, '$.force_pw_update') NOT LIKE '%%1%%'
 EOF
@@ -106,12 +107,14 @@ cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve
 # Check permissions of vmail directory.
 # Do not do this every start-up, it may take a very long time. So we use a stat check here.
 if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi
+if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi
 
 # Create random master for SOGo sieve features
 RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
 RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
-echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd
-echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds
+
+echo ${RAND_USER}@mailcow.local:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd
+echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds
 
 # 401 is user dovecot
 if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then
@@ -138,7 +141,10 @@ touch /etc/crontab /etc/cron.*/*
 
 # Clean stopped imapsync jobs
 rm -f /tmp/imapsync_busy.lock
-IMAPSYNC_TABLE=$(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
-[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
+IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
+[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
+
+# Collect SA rules once now
+/usr/local/bin/sa-rules.sh
 
 exec "$@"

+ 2 - 0
data/Dockerfiles/dovecot/maildir_gc.sh

@@ -0,0 +1,2 @@
+#/bin/bash
+[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r {} \;

+ 0 - 1
data/Dockerfiles/dovecot/postlogin.sh

@@ -1,4 +1,3 @@
 #!/bin/sh
-
 export MASTER_USER=$USER
 exec "$@"

+ 2 - 2
data/Dockerfiles/dovecot/rspamd-pipe-ham

@@ -3,7 +3,7 @@ FILE=/tmp/mail$$
 cat > $FILE
 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
 
-cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnham
-cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd
+cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham
+cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd
 
 exit 0

+ 2 - 2
data/Dockerfiles/dovecot/rspamd-pipe-spam

@@ -3,7 +3,7 @@ FILE=/tmp/mail$$
 cat > $FILE
 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
 
-cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnspam
-cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd
+cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam
+cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd
 
 exit 0

+ 25 - 0
data/Dockerfiles/dovecot/sa-rules.sh

@@ -0,0 +1,25 @@
+#!/bin/bash
+[[ ! -d /tmp/sa-rules-heinlein ]] && mkdir -p /tmp/sa-rules-heinlein
+if [[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]]; then
+  HASH_SA_RULES=0
+else
+  HASH_SA_RULES=$(cat /etc/rspamd/custom/sa-rules-heinlein | md5sum | cut -d' ' -f1)
+fi
+
+curl --connect-timeout 15 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"').tar.gz --output /tmp/sa-rules.tar.gz
+if [[ -f /tmp/sa-rules.tar.gz ]]; then
+  tar xfvz /tmp/sa-rules.tar.gz -C /tmp/sa-rules-heinlein
+  # create complete list of rules in a single file
+  cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules-heinlein
+  # Only restart rspamd-mailcow when rules changed
+  if [[ $(cat /etc/rspamd/custom/sa-rules-heinlein | md5sum | cut -d' ' -f1) != ${HASH_SA_RULES} ]]; then
+    CONTAINER_NAME=rspamd-mailcow
+    CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \
+      jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | \
+      jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | .id")
+    if [[ ! -z ${CONTAINER_ID} ]]; then
+      curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart
+    fi
+  fi
+fi
+rm -rf /tmp/sa-rules-heinlein /tmp/sa-rules.tar.gz

+ 6 - 7
data/Dockerfiles/dovecot/trim_logs.sh

@@ -1,8 +1,7 @@
 #!/bin/bash
-
-redis-cli -h redis LTRIM ACME_LOG 0 LOG_LINES
-redis-cli -h redis LTRIM POSTFIX_MAILLOG 0 LOG_LINES
-redis-cli -h redis LTRIM DOVECOT_MAILLOG 0 LOG_LINES
-redis-cli -h redis LTRIM SOGO_LOG 0 LOG_LINES
-redis-cli -h redis LTRIM NETFILTER_LOG 0 LOG_LINES
-redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 LOG_LINES
+/usr/bin/redis-cli -h redis LTRIM ACME_LOG 0 LOG_LINES
+/usr/bin/redis-cli -h redis LTRIM POSTFIX_MAILLOG 0 LOG_LINES
+/usr/bin/redis-cli -h redis LTRIM DOVECOT_MAILLOG 0 LOG_LINES
+/usr/bin/redis-cli -h redis LTRIM SOGO_LOG 0 LOG_LINES
+/usr/bin/redis-cli -h redis LTRIM NETFILTER_LOG 0 LOG_LINES
+/usr/bin/redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 LOG_LINES

+ 2 - 2
data/Dockerfiles/phpfpm/Dockerfile

@@ -1,11 +1,11 @@
 FROM php:7.2-fpm-alpine3.7
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
-ENV APCU_PECL 5.1.11
+ENV APCU_PECL 5.1.12
 ENV IMAGICK_PECL 3.4.3
 ENV MAILPARSE_PECL 3.0.2
 ENV MEMCACHED_PECL 3.0.4
-ENV REDIS_PECL 4.0.2
+ENV REDIS_PECL 4.1.1
 
 RUN apk add -U --no-cache autoconf \
   bash \

+ 6 - 4
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -4,11 +4,13 @@ set -e
 function array_by_comma { local IFS=","; echo "$*"; }
 
 # Wait for containers
-while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do
+while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
+  echo "Waiting for SQL..."
   sleep 2
 done
 
 until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do
+  echo "Waiting for Redis..."
   sleep 2
 done
 
@@ -18,11 +20,11 @@ redis-cli -h redis-mailcow DEL DOMAIN_MAP
 while read line
 do
   DOMAIN_ARR+=("$line")
-done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
+done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
 while read line
 do
   DOMAIN_ARR+=("$line")
-done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
+done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
 
 if [[ ! -z ${DOMAIN_ARR} ]]; then
 for domain in "${DOMAIN_ARR[@]}"; do
@@ -48,7 +50,7 @@ if [[ ${API_ALLOW_FROM} != "invalid" ]] && \
   done
   VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
   if [[ ! -z ${VALIDATED_IPS} ]]; then
-    mysql --host mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 INSERT INTO api (username, api_key, active, allow_from)
 SELECT username, "${API_KEY}", '1', "${VALIDATED_IPS}" FROM admin WHERE superadmin='1' AND active='1'
 ON DUPLICATE KEY UPDATE active = '1', allow_from = "${VALIDATED_IPS}", api_key = "${API_KEY}";

+ 23 - 15
data/Dockerfiles/postfix/postfix.sh

@@ -14,7 +14,7 @@ newaliases;
 cat <<EOF > /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT DISTINCT
   CASE WHEN '%d' IN (
@@ -29,10 +29,18 @@ query = SELECT DISTINCT
   END AS result;
 EOF
 
+cat <<EOF > /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
+user = ${DBUSER}
+password = ${DBPASS}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = ${DBNAME}
+query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_override WHERE active = '1' AND dest = '%s'
+EOF
+
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT IF(EXISTS(
   SELECT 'TLS_ACTIVE' FROM alias
@@ -49,7 +57,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps
   FROM (
@@ -80,7 +88,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
   WHERE id IN (
@@ -96,7 +104,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT goto FROM alias, alias_domain
   WHERE alias_domain.alias_domain = '%d'
@@ -107,7 +115,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT username FROM mailbox, alias_domain
   WHERE alias_domain.alias_domain = '%d'
@@ -119,7 +127,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT goto FROM alias
   WHERE address='%s'
@@ -129,7 +137,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT bcc_dest FROM bcc_maps
   WHERE local_dest='%s'
@@ -140,7 +148,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT bcc_dest FROM bcc_maps
   WHERE local_dest='%s'
@@ -151,7 +159,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT new_dest FROM recipient_maps
   WHERE old_dest='%s'
@@ -161,7 +169,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1'
   UNION
@@ -174,7 +182,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'
 EOF
@@ -182,7 +190,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1'
 EOF
@@ -190,7 +198,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 # First select queries domain and alias_domain to determine if domains are active.
 query = SELECT goto FROM alias
@@ -231,7 +239,7 @@ EOF
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}
-hosts = mysql
+hosts = unix:/var/run/mysqld/mysqld.sock
 dbname = ${DBNAME}
 query = SELECT goto FROM spamalias
   WHERE address='%s'

+ 2 - 2
data/Dockerfiles/postfix/rspamd-pipe-ham

@@ -3,7 +3,7 @@ FILE=/tmp/mail$$
 cat > $FILE
 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
 
-cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnham
-cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd
+cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham
+cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd
 
 exit 0

+ 2 - 2
data/Dockerfiles/postfix/rspamd-pipe-spam

@@ -3,7 +3,7 @@ FILE=/tmp/mail$$
 cat > $FILE
 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
 
-cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnspam
-cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd
+cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam
+cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd
 
 exit 0

+ 1 - 1
data/Dockerfiles/rspamd/Dockerfile

@@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y \
 	&& mkdir -p /run/rspamd \
 	&& chown _rspamd:_rspamd /run/rspamd
 
-COPY settings.conf /etc/rspamd/modules.d/settings.conf
+COPY settings.conf /etc/rspamd/settings.conf
 COPY docker-entrypoint.sh /docker-entrypoint.sh
 
 ENTRYPOINT ["/docker-entrypoint.sh"]

+ 4 - 1
data/Dockerfiles/rspamd/docker-entrypoint.sh

@@ -1,6 +1,9 @@
 #!/bin/bash
 
-chown -R _rspamd:_rspamd /var/lib/rspamd
+chown -R _rspamd:_rspamd /var/lib/rspamd /etc/rspamd/local.d /etc/rspamd/override.d /etc/rspamd/custom
+chmod 755 /var/lib/rspamd
 [[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Placeholder' > /etc/rspamd/override.d/worker-controller-password.inc
+chown _rspamd:_rspamd /etc/rspamd/override.d/worker-controller-password.inc
+[[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]] && echo '# to be auto-filled by dovecot-mailcow' > /etc/rspamd/custom/sa-rules-heinlein
 
 exec "$@"

+ 0 - 152
data/Dockerfiles/rspamd/lua_util.lua

@@ -1,152 +0,0 @@
-local exports = {}
-local lpeg = require 'lpeg'
-
-local split_grammar = {}
-local function rspamd_str_split(s, sep)
-  local gr = split_grammar[sep]
-
-  if not gr then
-    local _sep = lpeg.P(sep)
-    local elem = lpeg.C((1 - _sep)^0)
-    local p = lpeg.Ct(elem * (_sep * elem)^0)
-    gr = p
-    split_grammar[sep] = gr
-  end
-
-  return gr:match(s)
-end
-
-exports.rspamd_str_split = rspamd_str_split
-
-local space = lpeg.S' \t\n\v\f\r'
-local nospace = 1 - space
-local ptrim = space^0 * lpeg.C((space^0 * nospace^1)^0)
-local match = lpeg.match
-exports.rspamd_str_trim = function(s)
-  return match(ptrim, s)
-end
-
--- Robert Jay Gould http://lua-users.org/wiki/SimpleRound
-exports.round = function(num, numDecimalPlaces)
-  local mult = 10^(numDecimalPlaces or 0)
-  return math.floor(num * mult) / mult
-end
-
-exports.template = function(tmpl, keys)
-  local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" }
-  local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) }
-  local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") }
-
-  local template_grammar = lpeg.Cs((var + var_braced + 1)^0)
-
-  return lpeg.match(template_grammar, tmpl)
-end
-
-exports.remove_email_aliases = function(email_addr)
-  local function check_gmail_user(addr)
-    -- Remove all points
-    local no_dots_user = string.gsub(addr.user, '%.', '')
-    local cap, pluses = string.match(no_dots_user, '^([^%+][^%+]*)(%+.*)$')
-    if cap then
-      return cap, rspamd_str_split(pluses, '+'), nil
-    elseif no_dots_user ~= addr.user then
-      return no_dots_user,{},nil
-    end
-
-    return nil
-  end
-
-  local function check_address(addr)
-    if addr.user then
-      local cap, pluses = string.match(addr.user, '^([^%+][^%+]*)(%+.*)$')
-      if cap then
-        return cap, rspamd_str_split(pluses, '+'), nil
-      end
-    end
-
-    return nil
-  end
-
-  local function set_addr(addr, new_user, new_domain)
-    if new_user then
-      addr.user = new_user
-    end
-    if new_domain then
-      addr.domain = new_domain
-    end
-
-    if addr.domain then
-      addr.addr = string.format('%s@%s', addr.user, addr.domain)
-    else
-      addr.addr = string.format('%s@', addr.user)
-    end
-
-    if addr.name and #addr.name > 0 then
-      addr.raw = string.format('"%s" <%s>', addr.name, addr.addr)
-    else
-      addr.raw = string.format('<%s>', addr.addr)
-    end
-  end
-
-  local function check_gmail(addr)
-    local nu, tags, nd = check_gmail_user(addr)
-
-    if nu then
-      return nu, tags, nd
-    end
-
-    return nil
-  end
-
-  local function check_googlemail(addr)
-    local nd = 'gmail.com'
-    local nu, tags = check_gmail_user(addr)
-
-    if nu then
-      return nu, tags, nd
-    end
-
-    return nil, nil, nd
-  end
-
-  local specific_domains = {
-    ['gmail.com'] = check_gmail,
-    ['googlemail.com'] = check_googlemail,
-  }
-
-  if email_addr then
-    if email_addr.domain and specific_domains[email_addr.domain] then
-      local nu, tags, nd = specific_domains[email_addr.domain](email_addr)
-      if nu or nd then
-        set_addr(email_addr, nu, nd)
-
-        return nu, tags
-      end
-    else
-      local nu, tags, nd = check_address(email_addr)
-      if nu or nd then
-        set_addr(email_addr, nu, nd)
-
-        return nu, tags
-      end
-    end
-
-    return nil
-  end
-end
-
-exports.is_rspamc_or_controller = function(task)
-  local ua = task:get_request_header('User-Agent') or ''
-  local pwd = task:get_request_header('Password')
-  local is_rspamc = false
-  if tostring(ua) == 'rspamc' or pwd then is_rspamc = true end
-
-  return is_rspamc
-end
-
-local unpack_function = table.unpack or unpack
-exports.unpack = function(t)
-  return unpack_function(t)
-end
-
-return exports

+ 0 - 674
data/Dockerfiles/rspamd/ratelimit.lua

@@ -1,674 +0,0 @@
---[[
-Copyright (c) 2011-2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
-Copyright (c) 2016-2017, Andrew Lewis <nerf@judo.za.org>
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-]]--
-
-if confighelp then
-  return
-end
-
--- A plugin that implements ratelimits using redis
-
-local E = {}
-local N = 'ratelimit'
-local redis_params
--- Senders that are considered as bounce
-local settings = {
-  bounce_senders = { 'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon' },
--- Do not check ratelimits for these recipients
-  whitelisted_rcpts = { 'postmaster', 'mailer-daemon' },
-  prefix = 'RL',
-  ham_factor_rate = 1.01,
-  spam_factor_rate = 0.99,
-  ham_factor_burst = 1.02,
-  spam_factor_burst = 0.98,
-  max_rate_mult = 5,
-  max_bucket_mult = 10,
-  expire = 60 * 60 * 24 * 2, -- 2 days by default
-  limits = {},
-  allow_local = false,
-}
-
--- Checks bucket, updating it if needed
--- KEYS[1] - prefix to update, e.g. RL_<triplet>_<seconds>
--- KEYS[2] - current time in milliseconds
--- KEYS[3] - bucket leak rate (messages per millisecond)
--- KEYS[4] - bucket burst
--- KEYS[5] - expire for a bucket
--- return 1 if message should be ratelimited and 0 if not
--- Redis keys used:
---   l - last hit
---   b - current burst
---   dr - current dynamic rate multiplier (*10000)
---   db - current dynamic burst multiplier (*10000)
-local bucket_check_script = [[
-  local last = redis.call('HGET', KEYS[1], 'l')
-  local now = tonumber(KEYS[2])
-  local dynr, dynb = 0, 0
-  if not last then
-    -- New bucket
-    redis.call('HSET', KEYS[1], 'l', KEYS[2])
-    redis.call('HSET', KEYS[1], 'b', '0')
-    redis.call('HSET', KEYS[1], 'dr', '10000')
-    redis.call('HSET', KEYS[1], 'db', '10000')
-    redis.call('EXPIRE', KEYS[1], KEYS[5])
-    return {0, 0, 1, 1}
-  end
-
-  last = tonumber(last)
-  local burst = tonumber(redis.call('HGET', KEYS[1], 'b'))
-  -- Perform leak
-  if burst > 0 then
-   if last < tonumber(KEYS[2]) then
-    local rate = tonumber(KEYS[3])
-    dynr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000.0
-    rate = rate * dynr
-    local leaked = ((now - last) * rate)
-    burst = burst - leaked
-    redis.call('HINCRBYFLOAT', KEYS[1], 'b', -(leaked))
-   end
-  else
-   burst = 0
-   redis.call('HSET', KEYS[1], 'b', '0')
-  end
-
-  dynb = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000.0
-
-  if (burst + 1) * dynb > tonumber(KEYS[4]) then
-   return {1, tostring(burst), tostring(dynr), tostring(dynb)}
-  end
-
-  return {0, tostring(burst), tostring(dynr), tostring(dynb)}
-]]
-local bucket_check_id
-
-
--- Updates a bucket
--- KEYS[1] - prefix to update, e.g. RL_<triplet>_<seconds>
--- KEYS[2] - current time in milliseconds
--- KEYS[3] - dynamic rate multiplier
--- KEYS[4] - dynamic burst multiplier
--- KEYS[5] - max dyn rate (min: 1/x)
--- KEYS[6] - max burst rate (min: 1/x)
--- KEYS[7] - expire for a bucket
--- Redis keys used:
---   l - last hit
---   b - current burst
---   dr - current dynamic rate multiplier
---   db - current dynamic burst multiplier
-local bucket_update_script = [[
-  local last = redis.call('HGET', KEYS[1], 'l')
-  local now = tonumber(KEYS[2])
-  if not last then
-    -- New bucket
-    redis.call('HSET', KEYS[1], 'l', KEYS[2])
-    redis.call('HSET', KEYS[1], 'b', '1')
-    redis.call('HSET', KEYS[1], 'dr', '10000')
-    redis.call('HSET', KEYS[1], 'db', '10000')
-    redis.call('EXPIRE', KEYS[1], KEYS[7])
-    return {1, 1, 1}
-  end
-
-  local burst = tonumber(redis.call('HGET', KEYS[1], 'b'))
-  local db = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000
-  local dr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000
-
-  if dr < tonumber(KEYS[5]) and dr > 1.0 / tonumber(KEYS[5]) then
-    dr = dr * tonumber(KEYS[3])
-    redis.call('HSET', KEYS[1], 'dr', tostring(math.floor(dr * 10000)))
-  end
-
-  if db < tonumber(KEYS[6]) and db > 1.0 / tonumber(KEYS[6]) then
-    db = db * tonumber(KEYS[4])
-    redis.call('HSET', KEYS[1], 'db', tostring(math.floor(db * 10000)))
-  end
-
-  redis.call('HINCRBYFLOAT', KEYS[1], 'b', 1)
-  redis.call('HSET', KEYS[1], 'l', KEYS[2])
-  redis.call('EXPIRE', KEYS[1], KEYS[7])
-
-  return {tostring(burst), tostring(dr), tostring(db)}
-]]
-local bucket_update_id
-
--- message_func(task, limit_type, prefix, bucket)
-local message_func = function(_, limit_type, _, _)
-  return string.format('Ratelimit "%s" exceeded', limit_type)
-end
-
-local rspamd_logger = require "rspamd_logger"
-local rspamd_util = require "rspamd_util"
-local rspamd_lua_utils = require "lua_util"
-local lua_redis = require "lua_redis"
-local fun = require "fun"
-local lua_maps = require "lua_maps"
-local lua_util = require "lua_util"
-local rspamd_hash = require "rspamd_cryptobox_hash"
-
-
-local function load_scripts(cfg, ev_base)
-  bucket_check_id = lua_redis.add_redis_script(bucket_check_script, redis_params)
-  bucket_update_id = lua_redis.add_redis_script(bucket_update_script, redis_params)
-end
-
-local limit_parser
-local function parse_string_limit(lim, no_error)
-  local function parse_time_suffix(s)
-    if s == 's' then
-      return 1
-    elseif s == 'm' then
-      return 60
-    elseif s == 'h' then
-      return 3600
-    elseif s == 'd' then
-      return 86400
-    end
-  end
-  local function parse_num_suffix(s)
-    if s == '' then
-      return 1
-    elseif s == 'k' then
-      return 1000
-    elseif s == 'm' then
-      return 1000000
-    elseif s == 'g' then
-      return 1000000000
-    end
-  end
-  local lpeg = require "lpeg"
-
-  if not limit_parser then
-    local digit = lpeg.R("09")
-    limit_parser = {}
-    limit_parser.integer =
-    (lpeg.S("+-") ^ -1) *
-            (digit   ^  1)
-    limit_parser.fractional =
-    (lpeg.P(".")   ) *
-            (digit ^ 1)
-    limit_parser.number =
-    (limit_parser.integer *
-            (limit_parser.fractional ^ -1)) +
-            (lpeg.S("+-") * limit_parser.fractional)
-    limit_parser.time = lpeg.Cf(lpeg.Cc(1) *
-            (limit_parser.number / tonumber) *
-            ((lpeg.S("smhd") / parse_time_suffix) ^ -1),
-      function (acc, val) return acc * val end)
-    limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) *
-            (limit_parser.number / tonumber) *
-            ((lpeg.S("kmg") / parse_num_suffix) ^ -1),
-      function (acc, val) return acc * val end)
-    limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number *
-            (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) *
-            limit_parser.time)
-  end
-  local t = lpeg.match(limit_parser.limit, lim)
-
-  if t and t[1] and t[2] and t[2] ~= 0 then
-    return t[2], t[1]
-  end
-
-  if not no_error then
-    rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim)
-  end
-
-  return nil
-end
-
-local function parse_limit(name, data)
-  local buckets = {}
-  if type(data) == 'table' then
-    -- 3 cases here:
-    --  * old limit in format [burst, rate]
-    --  * vector of strings in Andrew's string format
-    --  * proper bucket table
-    if #data == 2 and tonumber(data[1]) and tonumber(data[2]) then
-      -- Old style ratelimit
-      rspamd_logger.warnx(rspamd_config, 'old style ratelimit for %s', name)
-      if tonumber(data[1]) > 0 and tonumber(data[2]) > 0 then
-        table.insert(buckets, {
-          burst = data[1],
-          rate = data[2]
-        })
-      elseif data[1] ~= 0 then
-        rspamd_logger.warnx(rspamd_config, 'invalid numbers for %s', name)
-      else
-        rspamd_logger.infox(rspamd_config, 'disable limit %s, burst is zero', name)
-      end
-    else
-      -- Recursively map parse_limit and flatten the list
-      fun.each(function(l)
-        -- Flatten list
-        for _,b in ipairs(l) do table.insert(buckets, b) end
-      end, fun.map(function(d) return parse_limit(d, name) end, data))
-    end
-  elseif type(data) == 'string' then
-    local rep_rate, burst = parse_string_limit(data)
-
-    if rep_rate and burst then
-      table.insert(buckets, {
-        burst = burst,
-        rate = 1.0 / rep_rate -- reciprocal
-      })
-    end
-  end
-
-  -- Filter valid
-  return fun.totable(fun.filter(function(val)
-    return type(val.burst) == 'number' and type(val.rate) == 'number'
-  end, buckets))
-end
-
---- Check whether this addr is bounce
-local function check_bounce(from)
-  return fun.any(function(b) return b == from end, settings.bounce_senders)
-end
-
-local keywords = {
-  ['ip'] = {
-    ['get_value'] = function(task)
-      local ip = task:get_ip()
-      if ip and ip:is_valid() then return tostring(ip) end
-      return nil
-    end,
-  },
-  ['rip'] = {
-    ['get_value'] = function(task)
-      local ip = task:get_ip()
-      if ip and ip:is_valid() and not ip:is_local() then return tostring(ip) end
-      return nil
-    end,
-  },
-  ['from'] = {
-    ['get_value'] = function(task)
-      local from = task:get_from(0)
-      if ((from or E)[1] or E).addr then
-        return string.lower(from[1]['addr'])
-      end
-      return nil
-    end,
-  },
-  ['bounce'] = {
-    ['get_value'] = function(task)
-      local from = task:get_from(0)
-      if not ((from or E)[1] or E).user then
-        return '_'
-      end
-      if check_bounce(from[1]['user']) then return '_' else return nil end
-    end,
-  },
-  ['asn'] = {
-    ['get_value'] = function(task)
-      local asn = task:get_mempool():get_variable('asn')
-      if not asn then
-        return nil
-      else
-        return asn
-      end
-    end,
-  },
-  ['user'] = {
-    ['get_value'] = function(task)
-      local auser = task:get_user()
-      if not auser then
-        return nil
-      else
-        return auser
-      end
-    end,
-  },
-  ['to'] = {
-    ['get_value'] = function(task)
-      return task:get_principal_recipient()
-    end,
-  },
-}
-
-local function gen_rate_key(task, rtype, bucket)
-  local key_t = {tostring(lua_util.round(100000.0 / bucket.burst))}
-  local key_keywords = lua_util.str_split(rtype, '_')
-  local have_user = false
-
-  for _, v in ipairs(key_keywords) do
-    local ret
-
-    if keywords[v] and type(keywords[v]['get_value']) == 'function' then
-      ret = keywords[v]['get_value'](task)
-    end
-    if not ret then return nil end
-    if v == 'user' then have_user = true end
-    if type(ret) ~= 'string' then ret = tostring(ret) end
-    table.insert(key_t, ret)
-  end
-
-  if have_user and not task:get_user() then
-    return nil
-  end
-
-  return table.concat(key_t, ":")
-end
-
-local function make_prefix(redis_key, name, bucket)
-  local hash_len = 24
-  if hash_len > #redis_key then hash_len = #redis_key end
-  local hash = settings.prefix ..
-      string.sub(rspamd_hash.create(redis_key):base32(), 1, hash_len)
-  -- Fill defaults
-  if not bucket.spam_factor_rate then
-    bucket.spam_factor_rate = settings.spam_factor_rate
-  end
-  if not bucket.ham_factor_rate then
-    bucket.ham_factor_rate = settings.ham_factor_rate
-  end
-  if not bucket.spam_factor_burst then
-    bucket.spam_factor_burst = settings.spam_factor_burst
-  end
-  if not bucket.ham_factor_burst then
-    bucket.ham_factor_burst = settings.ham_factor_burst
-  end
-
-  return {
-    bucket = bucket,
-    name = name,
-    hash = hash
-  }
-end
-
-local function limit_to_prefixes(task, k, v, prefixes)
-  local n = 0
-  for _,bucket in ipairs(v) do
-    local prefix = gen_rate_key(task, k, bucket)
-
-    if prefix then
-      prefixes[prefix] = make_prefix(prefix, k, bucket)
-      n = n + 1
-    end
-  end
-
-  return n
-end
-
-local function ratelimit_cb(task)
-  if not settings.allow_local and
-          rspamd_lua_utils.is_rspamc_or_controller(task) then return end
-
-  -- Get initial task data
-  local ip = task:get_from_ip()
-  if ip and ip:is_valid() and settings.whitelisted_ip then
-    if settings.whitelisted_ip:get_key(ip) then
-      -- Do not check whitelisted ip
-      rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP')
-      return
-    end
-  end
-  -- Parse all rcpts
-  local rcpts = task:get_recipients()
-  local rcpts_user = {}
-  if rcpts then
-    fun.each(function(r)
-      fun.each(function(type) table.insert(rcpts_user, r[type]) end, {'user', 'addr'})
-    end, rcpts)
-
-    if fun.any(function(r) return settings.whitelisted_rcpts:get_key(r) end, rcpts_user) then
-      rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient')
-      return
-    end
-  end
-  -- Get user (authuser)
-  if settings.whitelisted_user then
-    local auser = task:get_user()
-    if settings.whitelisted_user:get_key(auser) then
-      rspamd_logger.infox(task, 'skip ratelimit for whitelisted user')
-      return
-    end
-  end
-  -- Now create all ratelimit prefixes
-  local prefixes = {}
-  local nprefixes = 0
-
-  for k,v in pairs(settings.limits) do
-    nprefixes = nprefixes + limit_to_prefixes(task, k, v, prefixes)
-  end
-
-  for k, hdl in pairs(settings.custom_keywords or E) do
-    local ret, redis_key, bd = pcall(hdl, task)
-
-    if ret then
-      local bucket = parse_limit(k, bd)
-      if bucket[1] then
-        prefixes[redis_key] = make_prefix(redis_key, k, bucket[1])
-      end
-      nprefixes = nprefixes + 1
-    else
-      rspamd_logger.errx(task, 'cannot call handler for %s: %s',
-          k, redis_key)
-    end
-  end
-
-  local function gen_check_cb(prefix, bucket, lim_name)
-    return function(err, data)
-      if err then
-        rspamd_logger.errx('cannot check limit %s: %s %s', prefix, err, data)
-      elseif type(data) == 'table' and data[1] and data[1] == 1 then
-        -- set symbol only and do NOT soft reject
-        if settings.symbol then
-          task:insert_result(settings.symbol, 0.0, lim_name .. "(" .. prefix .. ")")
-          rspamd_logger.infox(task,
-              'set_symbol_only: ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)',
-              lim_name, prefix,
-              bucket.burst, bucket.rate,
-              data[2], data[3], data[4])
-          return
-        -- set INFO symbol and soft reject
-        elseif settings.info_symbol then
-          task:insert_result(settings.info_symbol, 1.0,
-              lim_name .. "(" .. prefix .. ")")
-        end
-        rspamd_logger.infox(task,
-            'ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)',
-            lim_name, prefix,
-            bucket.burst, bucket.rate,
-            data[2], data[3], data[4])
-        task:set_pre_result('soft reject',
-                message_func(task, lim_name, prefix, bucket))
-      end
-    end
-  end
-
-  -- Don't do anything if pre-result has been already set
-  if task:has_pre_result() then return end
-
-  if nprefixes > 0 then
-    -- Save prefixes to the cache to allow update
-    task:cache_set('ratelimit_prefixes', prefixes)
-    local now = rspamd_util.get_time()
-    now = lua_util.round(now * 1000.0) -- Get milliseconds
-    -- Now call check script for all defined prefixes
-
-    for pr,value in pairs(prefixes) do
-      local bucket = value.bucket
-      local rate = (bucket.rate) / 1000.0 -- Leak rate in messages/ms
-      rspamd_logger.debugm(N, task, "check limit %s:%s -> %s (%s/%s)",
-          value.name, pr, value.hash, bucket.burst, bucket.rate)
-      lua_redis.exec_redis_script(bucket_check_id,
-              {key = value.hash, task = task, is_write = true},
-              gen_check_cb(pr, bucket, value.name),
-              {value.hash, tostring(now), tostring(rate), tostring(bucket.burst),
-                  tostring(settings.expire)})
-    end
-  end
-end
-
-local function ratelimit_update_cb(task)
-  local prefixes = task:cache_get('ratelimit_prefixes')
-
-  if prefixes then
-    if task:has_pre_result() then
-      -- Already rate limited/greylisted, do nothing
-      rspamd_logger.debugm(N, task, 'pre-action has been set, do not update')
-      return
-    end
-
-    local is_spam = not (task:get_metric_action() == 'no action')
-
-    -- Update each bucket
-    for k, v in pairs(prefixes) do
-      local bucket = v.bucket
-      local function update_bucket_cb(err, data)
-        if err then
-          rspamd_logger.errx(task, 'cannot update rate bucket %s: %s',
-                  k, err)
-        else
-          rspamd_logger.debugm(N, task,
-              "updated limit %s:%s -> %s (%s/%s), burst: %s, dyn_rate: %s, dyn_burst: %s",
-              v.name, k, v.hash,
-              bucket.burst, bucket.rate,
-              data[1], data[2], data[3])
-        end
-      end
-      local now = rspamd_util.get_time()
-      now = lua_util.round(now * 1000.0) -- Get milliseconds
-      local mult_burst = bucket.ham_factor_burst or 1.0
-      local mult_rate = bucket.ham_factor_burst or 1.0
-
-      if is_spam then
-        mult_burst = bucket.spam_factor_burst or 1.0
-        mult_rate = bucket.spam_factor_rate or 1.0
-      end
-
-      lua_redis.exec_redis_script(bucket_update_id,
-              {key = v.hash, task = task, is_write = true},
-              update_bucket_cb,
-              {v.hash, tostring(now), tostring(mult_rate), tostring(mult_burst),
-               tostring(settings.max_rate_mult), tostring(settings.max_bucket_mult),
-               tostring(settings.expire)})
-    end
-  end
-end
-
-local opts = rspamd_config:get_all_opt(N)
-if opts then
-
-  settings = lua_util.override_defaults(settings, opts)
-
-  if opts['limit'] then
-    rspamd_logger.errx(rspamd_config, 'Legacy ratelimit config format no longer supported')
-  end
-
-  if opts['rates'] and type(opts['rates']) == 'table' then
-    -- new way of setting limits
-    fun.each(function(t, lim)
-      local buckets = parse_limit(t, lim)
-
-      if buckets and #buckets > 0 then
-        settings.limits[t] = buckets
-      end
-    end, opts['rates'])
-  end
-
-  local enabled_limits = fun.totable(fun.map(function(t)
-    return t
-  end, settings.limits))
-  rspamd_logger.infox(rspamd_config,
-          'enabled rate buckets: [%1]', table.concat(enabled_limits, ','))
-
-  -- Ret, ret, ret: stupid legacy stuff:
-  -- If we have a string with commas then load it as as static map
-  -- otherwise, apply normal logic of Rspamd maps
-
-  local wrcpts = opts['whitelisted_rcpts']
-  if type(wrcpts) == 'string' then
-    if string.find(wrcpts, ',') then
-      settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(
-        lua_util.rspamd_str_split(wrcpts, ','), 'set', 'Ratelimit whitelisted rcpts')
-    else
-      settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set',
-        'Ratelimit whitelisted rcpts')
-    end
-  elseif type(opts['whitelisted_rcpts']) == 'table' then
-    settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set',
-      'Ratelimit whitelisted rcpts')
-  else
-    -- Stupid default...
-    settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(
-        settings.whitelisted_rcpts, 'set', 'Ratelimit whitelisted rcpts')
-  end
-
-  if opts['whitelisted_ip'] then
-    settings.whitelisted_ip = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix',
-      'Ratelimit whitelist ip map')
-  end
-
-  if opts['whitelisted_user'] then
-    settings.whitelisted_user = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_user', 'set',
-      'Ratelimit whitelist user map')
-  end
-
-  settings.custom_keywords = {}
-  if opts['custom_keywords'] then
-    local ret, res_or_err = pcall(loadfile(opts['custom_keywords']))
-
-    if ret then
-      opts['custom_keywords'] = {}
-      if type(res_or_err) == 'table' then
-        for k,hdl in pairs(res_or_err) do
-          settings['custom_keywords'][k] = hdl
-        end
-      elseif type(res_or_err) == 'function' then
-        settings['custom_keywords']['custom'] = res_or_err
-      end
-    else
-      rspamd_logger.errx(rspamd_config, 'cannot execute %s: %s',
-          opts['custom_keywords'], res_or_err)
-      settings['custom_keywords'] = {}
-    end
-  end
-
-  if opts['message_func'] then
-    message_func = assert(load(opts['message_func']))()
-  end
-
-  redis_params = lua_redis.parse_redis_server('ratelimit')
-
-  if not redis_params then
-    rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
-    lua_util.disable_module(N, "redis")
-  else
-    local s = {
-      type = 'prefilter,nostat',
-      name = 'RATELIMIT_CHECK',
-      priority = 7,
-      callback = ratelimit_cb,
-      flags = 'empty',
-    }
-
-    if settings.symbol then
-      s.name = settings.symbol
-    elseif settings.info_symbol then
-      s.name = settings.info_symbol
-    end
-
-    rspamd_config:register_symbol(s)
-    rspamd_config:register_symbol {
-      type = 'idempotent',
-      name = 'RATELIMIT_UPDATE',
-      callback = ratelimit_update_cb,
-    }
-  end
-end
-
-rspamd_config:add_on_load(function(cfg, ev_base, worker)
-  load_scripts(cfg, ev_base)
-end)

+ 2 - 0
data/Dockerfiles/sogo/Dockerfile

@@ -49,4 +49,6 @@ COPY sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg
 COPY acl.diff /acl.diff
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 
+VOLUME /usr/lib/GNUstep/SOGo/
+
 RUN rm -rf /tmp/* /var/tmp/*

+ 19 - 19
data/Dockerfiles/sogo/bootstrap-sogo.sh

@@ -1,7 +1,7 @@
 #!/bin/bash
 
 # Wait for MySQL to warm-up
-while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do
+while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
   echo "Waiting for database to come up..."
   sleep 2
 done
@@ -15,10 +15,10 @@ done
 
 # Recreate view
 
-mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
+mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
 
 while [[ ${VIEW_OK} != 'OK' ]]; do
-  mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+  mysql --socket=/var/run/mysqld/mysqld.sock -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, if(json_extract(attributes, '$.force_pw_update') LIKE '%0%', password, 'invalid'), mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), 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, '($|,)')
@@ -26,7 +26,7 @@ LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.usern
 WHERE mailbox.active = '1'
 GROUP BY mailbox.username;
 EOF
-  if [[ ! -z $(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
+  if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
     VIEW_OK=OK
   else
     echo "Will retry to setup SOGo view in 3s"
@@ -37,11 +37,11 @@ done
 # Wait for static view table if missing after update and update content
 
 while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
-  if [[ ! -z $(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
+  if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
     STATIC_VIEW_OK=OK
     echo "Updating _sogo_static_view content..."
-    mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view SELECT * from sogo_view"
-    mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
+    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view SELECT * from sogo_view"
+    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
   else
     echo "Waiting for database initialization..."
     sleep 3
@@ -50,10 +50,10 @@ done
 
 # Recreate password update trigger
 
-mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
+mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
 
 while [[ ${TRIGGER_OK} != 'OK' ]]; do
-  mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+  mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 DELIMITER -
 CREATE TRIGGER sogo_update_password AFTER UPDATE ON _sogo_static_view
 FOR EACH ROW
@@ -63,7 +63,7 @@ END;
 -
 DELIMITER ;
 EOF
-  if [[ ! -z $(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then
+  if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then
     TRIGGER_OK=OK
   else
     echo "Will retry to setup SOGo password update trigger in 3s"
@@ -81,19 +81,19 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
 <plist version="0.9">
 <dict>
     <key>OCSAclURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_acl</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
     <key>OCSCacheFolderURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_cache_folder</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
     <key>OCSEMailAlarmsFolderURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_alarms_folder</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_alarms_folder</string>
     <key>OCSFolderInfoURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_folder_info</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_folder_info</string>
     <key>OCSSessionsFolderURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_sessions_folder</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_sessions_folder</string>
     <key>OCSStoreURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_store</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_store</string>
     <key>SOGoProfileURL</key>
-    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_user_profile</string>
+    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_user_profile</string>
     <key>SOGoTimeZone</key>
     <string>${TZ}</string>
     <key>domains</key>
@@ -138,11 +138,11 @@ while read line
                     <key>prependPasswordScheme</key>
                     <string>YES</string>
                     <key>viewURL</key>
-                    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/_sogo_static_view</string>
+                    <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/_sogo_static_view</string>
                 </dict>
             </array>
         </dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
-done < <(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain;" -B -N)
+done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain;" -B -N)
 
 # Generate footer
 echo '    </dict>

+ 0 - 7
data/Dockerfiles/sogo/supervisord.conf

@@ -16,13 +16,6 @@ command=/usr/sbin/cron -f
 autorestart=true
 priority=2
 
-[program:sogo-webres]
-command=/usr/bin/python -u -m SimpleHTTPServer 9192
-directory=/usr/lib/GNUstep/SOGo/
-user=sogo
-autorestart=true
-priority=4
-
 [program:bootstrap-sogo]
 command=/bootstrap-sogo.sh
 stdout_logfile=/dev/stdout

+ 1 - 0
data/Dockerfiles/unbound/Dockerfile

@@ -10,6 +10,7 @@ RUN apk add --update --no-cache \
 	drill \
 	&& curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \
 	&& chown root:unbound /etc/unbound \
+  && adduser unbound tty \
 	&& chmod 775 /etc/unbound
 
 EXPOSE 53/udp 53/tcp

+ 4 - 1
data/Dockerfiles/unbound/docker-entrypoint.sh

@@ -1,8 +1,11 @@
 #!/bin/bash
 
+echo "Setting console permissions..."
+chown root:tty /dev/console
+chmod g+rw /dev/console
 echo "Receiving anchor key..."
 /usr/sbin/unbound-anchor -a /etc/unbound/trusted-key.key
 echo "Receiving root hints..."
 curl -#o /etc/unbound/root.hints https://www.internic.net/domain/named.cache
-
+/usr/sbin/unbound-control-setup
 exec "$@"

+ 23 - 19
data/Dockerfiles/watchdog/watchdog.sh

@@ -59,28 +59,34 @@ function mail_error() {
   log_msg "Sent notification email to ${1}"
 }
 
-
 get_container_ip() {
   # ${1} is container
   CONTAINER_ID=()
+  CONTAINER_IPS=()
   CONTAINER_IP=
   LOOP_C=1
   until [[ ${CONTAINER_IP} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || [[ ${LOOP_C} -gt 5 ]]; do
     sleep 0.5
     # get long container id for exact match
-    CONTAINER_ID=($(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | .id"))
+    CONTAINER_ID=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | .id"))
     # returned id can have multiple elements (if scaled), shuffle for random test
     CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
     if [[ ! -z ${CONTAINER_ID} ]]; then
       for matched_container in "${CONTAINER_ID[@]}"; do
-        CONTAINER_IP=$(curl --silent http://dockerapi:8080/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')
-        # grep will do nothing if one of these vars is empty
-        [[ -z ${CONTAINER_IP} ]] && continue
-        [[ -z ${IPV4_NETWORK} ]] && continue
-        # only return ips that are part of our network
-        if ! grep -q ${IPV4_NETWORK} <(echo ${CONTAINER_IP}); then
-          CONTAINER_IP=
-        fi
+        CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) 
+        for ip_match in "${CONTAINER_IPS[@]}"; do
+          # grep will do nothing if one of these vars is empty
+          [[ -z ${ip_match} ]] && continue
+          [[ -z ${IPV4_NETWORK} ]] && continue
+          # only return ips that are part of our network
+          if ! grep -q ${IPV4_NETWORK} <(echo ${ip_match}); then
+            continue
+          else
+            CONTAINER_IP=${ip_match}
+            break
+          fi
+        done
+        [[ ! -z ${CONTAINER_IP} ]] && break
       done
     fi
     LOOP_C=$((LOOP_C + 1))
@@ -88,7 +94,6 @@ get_container_ip() {
   [[ ${LOOP_C} -gt 5 ]] && echo 240.0.0.0 || echo ${CONTAINER_IP}
 }
 
-# Check functions
 nginx_checks() {
   err_count=0
   diff_c=0
@@ -118,8 +123,8 @@ mysql_checks() {
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     host_ip=$(get_container_ip mysql-mailcow)
     err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_mysql -H ${host_ip} -P 3306 -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 1>&2; err_count=$(( ${err_count} + $? ))
-    /usr/lib/nagios/plugins/check_mysql_query -H ${host_ip} -P 3306 -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 1>&2; err_count=$(( ${err_count} + $? ))
+    /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 1>&2; err_count=$(( ${err_count} + $? ))
+    /usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 1>&2; err_count=$(( ${err_count} + $? ))
     [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
     [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
     progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
@@ -138,7 +143,6 @@ sogo_checks() {
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     host_ip=$(get_container_ip sogo-mailcow)
     err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /WebServerResources/css/theme-default.css -p 9192 -R md-default-theme 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 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} ))
@@ -222,7 +226,7 @@ rspamd_checks() {
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     host_ip=$(get_container_ip rspamd-mailcow)
     err_c_cur=${err_count}
-    SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/scan -d '
+    SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan -d '
 To: null@localhost
 From: watchdog@localhost
 
@@ -338,12 +342,12 @@ done
 # Monitor dockerapi
 (
 while true; do
-  while nc -z dockerapi 8080; do
+  while nc -z dockerapi 443; do
     sleep 3
   done
   log_msg "Cannot find dockerapi-mailcow, waiting to recover..."
   kill -STOP ${BACKGROUND_TASKS[*]}
-  until nc -z dockerapi 8080; do
+  until nc -z dockerapi 443; do
     sleep 3
   done
   kill -CONT ${BACKGROUND_TASKS[*]}
@@ -358,10 +362,10 @@ while true; do
   if [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
     kill -STOP ${BACKGROUND_TASKS[*]}
     sleep 3
-    CONTAINER_ID=$(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id")
+    CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id")
     if [[ ! -z ${CONTAINER_ID} ]]; then
       log_msg "Sending restart command to ${CONTAINER_ID}..."
-      curl --silent -XPOST http://dockerapi:8080/containers/${CONTAINER_ID}/restart
+      curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart
     fi
     log_msg "Wait for restarted container to settle and continue watching..."
     sleep 30s

+ 192 - 0
data/assets/mysql/docker-entrypoint.sh

@@ -0,0 +1,192 @@
+#!/bin/bash
+set -eo pipefail
+shopt -s nullglob
+
+openssl req -x509 -sha256 -newkey rsa:2048 -keyout /var/lib/mysql/sql.key -out /var/lib/mysql/sql.crt -days 3650 -nodes -subj '/CN=mysql'
+
+# if command starts with an option, prepend mysqld
+if [ "${1:0:1}" = '-' ]; then
+	set -- mysqld "$@"
+fi
+
+# skip setup if they want an option that stops mysqld
+wantHelp=
+for arg; do
+	case "$arg" in
+		-'?'|--help|--print-defaults|-V|--version)
+			wantHelp=1
+			break
+			;;
+	esac
+done
+
+# usage: file_env VAR [DEFAULT]
+#    ie: file_env 'XYZ_DB_PASSWORD' 'example'
+# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
+#  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
+file_env() {
+	local var="$1"
+	local fileVar="${var}_FILE"
+	local def="${2:-}"
+	if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
+		echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
+		exit 1
+	fi
+	local val="$def"
+	if [ "${!var:-}" ]; then
+		val="${!var}"
+	elif [ "${!fileVar:-}" ]; then
+		val="$(< "${!fileVar}")"
+	fi
+	export "$var"="$val"
+	unset "$fileVar"
+}
+
+_check_config() {
+	toRun=( "$@" --verbose --help --log-bin-index="$(mktemp -u)" )
+	if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then
+		cat >&2 <<-EOM
+
+			ERROR: mysqld failed while attempting to check config
+			command was: "${toRun[*]}"
+
+			$errors
+		EOM
+		exit 1
+	fi
+}
+
+# Fetch value from server config
+# We use mysqld --verbose --help instead of my_print_defaults because the
+# latter only show values present in config files, and not server defaults
+_get_config() {
+	local conf="$1"; shift
+	"$@" --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null | awk '$1 == "'"$conf"'" { print $2; exit }'
+}
+
+# allow the container to be started with `--user`
+if [ "$1" = 'mysqld' -a -z "$wantHelp" -a "$(id -u)" = '0' ]; then
+	_check_config "$@"
+	DATADIR="$(_get_config 'datadir' "$@")"
+	mkdir -p "$DATADIR"
+	chown -R mysql:mysql "$DATADIR"
+	exec gosu mysql "$BASH_SOURCE" "$@"
+fi
+
+if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then
+	# still need to check config, container may have started with --user
+	_check_config "$@"
+	# Get config
+	DATADIR="$(_get_config 'datadir' "$@")"
+
+	if [ ! -d "$DATADIR/mysql" ]; then
+		file_env 'MYSQL_ROOT_PASSWORD'
+		if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
+			echo >&2 'error: database is uninitialized and password option is not specified '
+			echo >&2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD'
+			exit 1
+		fi
+
+		mkdir -p "$DATADIR"
+
+		echo 'Initializing database'
+		# "Other options are passed to mysqld." (so we pass all "mysqld" arguments directly here)
+		mysql_install_db --datadir="$DATADIR" --rpm "${@:2}"
+		echo 'Database initialized'
+
+		SOCKET="$(_get_config 'socket' "$@")"
+		"$@" --skip-networking --socket="${SOCKET}" &
+		pid="$!"
+
+		mysql=( mysql --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" )
+
+		for i in {30..0}; do
+			if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
+				break
+			fi
+			echo 'MySQL init process in progress...'
+			sleep 1
+		done
+		if [ "$i" = 0 ]; then
+			echo >&2 'MySQL init process failed.'
+			exit 1
+		fi
+
+		if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then
+			# sed is for https://bugs.mysql.com/bug.php?id=20545
+			mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql
+		fi
+
+		if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
+			export MYSQL_ROOT_PASSWORD="$(pwgen -1 32)"
+			echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD"
+		fi
+
+		rootCreate=
+		# default root to listen for connections from anywhere
+		file_env 'MYSQL_ROOT_HOST' '%'
+		if [ ! -z "$MYSQL_ROOT_HOST" -a "$MYSQL_ROOT_HOST" != 'localhost' ]; then
+			# no, we don't care if read finds a terminating character in this heredoc
+			# https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151
+			read -r -d '' rootCreate <<-EOSQL || true
+				CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
+				GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ;
+			EOSQL
+		fi
+
+		"${mysql[@]}" <<-EOSQL
+			-- What's done in this file shouldn't be replicated
+			--  or products like mysql-fabric won't work
+			SET @@SESSION.SQL_LOG_BIN=0;
+
+			DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ;
+			SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ;
+			GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ;
+			${rootCreate}
+			DROP DATABASE IF EXISTS test ;
+			FLUSH PRIVILEGES ;
+		EOSQL
+
+		if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then
+			mysql+=( -p"${MYSQL_ROOT_PASSWORD}" )
+		fi
+
+		file_env 'MYSQL_DATABASE'
+		if [ "$MYSQL_DATABASE" ]; then
+			echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}"
+			mysql+=( "$MYSQL_DATABASE" )
+		fi
+
+		file_env 'MYSQL_USER'
+		file_env 'MYSQL_PASSWORD'
+		if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
+			echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}"
+
+			if [ "$MYSQL_DATABASE" ]; then
+				echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}"
+			fi
+		fi
+
+		echo
+		for f in /docker-entrypoint-initdb.d/*; do
+			case "$f" in
+				*.sh)     echo "$0: running $f"; . "$f" ;;
+				*.sql)    echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;;
+				*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;;
+				*)        echo "$0: ignoring $f" ;;
+			esac
+			echo
+		done
+
+		if ! kill -s TERM "$pid" || ! wait "$pid"; then
+			echo >&2 'MySQL init process failed.'
+			exit 1
+		fi
+
+		echo
+		echo 'MySQL init process done. Ready for start up.'
+		echo
+	fi
+fi
+
+exec "$@"

+ 11 - 8
data/conf/dovecot/dovecot.conf

@@ -14,7 +14,7 @@ disable_plaintext_auth = yes
 login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
 mail_home = /var/vmail/%d/%n
 mail_location = maildir:~/
-mail_plugins = quota acl zlib listescape #mail_crypt
+mail_plugins = quota acl zlib listescape mail_crypt mail_crypt_acl
 
 # Dovecot 2.2
 #ssl_protocols = !SSLv3
@@ -175,7 +175,7 @@ namespace {
     type = shared
     separator = /
     prefix = Shared/%%u/
-    location = maildir:%%h/:CONTROL=~/Shared/%%u:INDEXPVT=~/Shared/%%u
+    location = maildir:%%h/:INDEX=~/Shared/%%u;CONTROL=~/Shared/%%u
     subscriptions = no
     list = children
 }
@@ -223,7 +223,7 @@ service pop3-login {
 }
 service imap {
   executable = imap imap-postlogin
-  user = dovenull
+  user = vmail
   vsz_limit = 256 M
 }
 service managesieve {
@@ -244,11 +244,11 @@ userdb {
 }
 protocol imap {
   imap_metadata = yes
-  mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape #mail_crypt
+  mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl
 }
 mail_attribute_dict = file:%h/dovecot-attributes
 protocol lmtp {
-  mail_plugins = quota sieve acl zlib listescape #mail_crypt
+  mail_plugins = quota sieve acl zlib listescape mail_crypt mail_crypt_acl
   auth_socket_path = /usr/local/var/run/dovecot/auth-master
 }
 protocol sieve {
@@ -288,9 +288,12 @@ plugin {
   sieve_before = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
   sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
   sieve_after2 = /var/vmail/sieve/global.sieve
-  #mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
-  #mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
-  #mail_crypt_save_version = 2
+
+  # -- Global keys
+  mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
+  mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
+  mail_crypt_save_version = 2
+
   # Enable compression while saving, lz4 Dovecot v2.2.11+
   zlib_save = lz4
 }

+ 20 - 33
data/conf/nginx/site.conf

@@ -50,6 +50,18 @@ server {
   absolute_redirect off;
   root /web;
 
+  location / {
+    try_files $uri $uri/ @strip-ext;
+  }
+
+  location /edit {
+    rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2;
+  }
+
+  location @strip-ext {
+    rewrite ^(.*)$ $1.php last;
+  }
+
   location ~ ^/api/v1/(.*)$ {
     try_files $uri $uri/ /json_api.php?query=$1;
   }
@@ -120,12 +132,12 @@ server {
 
   location ^~ /Microsoft-Server-ActiveSync {
     include /etc/nginx/conf.d/sogo_eas.active;
-    proxy_connect_timeout 1000;
+    proxy_connect_timeout 4000;
     proxy_next_upstream timeout error;
-    proxy_send_timeout 1000;
-    proxy_read_timeout 1000;
+    proxy_send_timeout 4000;
+    proxy_read_timeout 4000;
     proxy_buffer_size 8k;
-    proxy_buffers 4 32k;
+    proxy_buffers 16 64k;
     proxy_temp_file_write_size 64k;
     proxy_busy_buffers_size 64k;
     proxy_set_header X-Real-IP $remote_addr;
@@ -156,44 +168,19 @@ server {
   }
 
   location /SOGo.woa/WebServerResources/ {
-    proxy_pass http://sogo:9192/WebServerResources/;
-    proxy_set_header Host $http_host;
-    proxy_cache sogo;
-    proxy_cache_valid 200 1d;
-    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
-    #alias /usr/lib/GNUstep/SOGo/WebServerResources/;
-    expires $expires;
-    allow all;
+    alias /usr/lib/GNUstep/SOGo/WebServerResources/;
   }
 
   location /.woa/WebServerResources/ {
-    proxy_pass http://sogo:9192/WebServerResources/;
-    proxy_set_header Host $http_host;
-    proxy_cache sogo;
-    proxy_cache_valid 200 1d;
-    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
-    #alias /usr/lib/GNUstep/SOGo/WebServerResources/;
-    expires $expires;
-    allow all;
+    alias /usr/lib/GNUstep/SOGo/WebServerResources/;
   }
 
   location /SOGo/WebServerResources/ {
-    proxy_pass http://sogo:9192/WebServerResources/;
-    proxy_set_header Host $http_host;
-    proxy_cache sogo;
-    proxy_cache_valid 200 1d;
-    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
-    #alias /usr/lib/GNUstep/SOGo/WebServerResources/;
-    allow all;
+    alias /usr/lib/GNUstep/SOGo/WebServerResources/;
   }
 
   location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
-    proxy_pass http://sogo:9192/$1.SOGo/Resources/$2;
-    proxy_set_header Host $http_host;
-    proxy_cache sogo;
-    proxy_cache_valid 200 1d;
-    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
-    #alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
+    alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
   }
 
   include /etc/nginx/conf.d/site.*.custom;

+ 1 - 1
data/conf/phpfpm/php-fpm.d/pools.conf

@@ -26,7 +26,7 @@ listen = [::]:9002
 access.log = /proc/self/fd/2
 clear_env = no
 catch_workers_output = yes
-php_admin_value[memory_limit] = 256M
+php_admin_value[memory_limit] = 512M
 php_admin_value[max_execution_time] = 1200
 php_admin_value[max_input_time] = 1200
 

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

@@ -20,7 +20,7 @@ broken_sasl_auth_clients = yes
 disable_vrfy_command = yes
 maximal_backoff_time = 1800s
 maximal_queue_lifetime = 1d
-message_size_limit = 26214400
+message_size_limit = 104857600
 milter_default_action = accept
 milter_protocol = 6
 minimal_backoff_time = 300s
@@ -41,6 +41,7 @@ postscreen_greet_wait = 3s
 postscreen_non_smtp_command_enable = no
 postscreen_pipelining_enable = no
 proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
@@ -127,3 +128,4 @@ smtp_sasl_auth_enable = yes
 smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf
 smtp_sasl_security_options = 
 smtp_sasl_mechanism_filter = plain, login
+smtp_tls_policy_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf

+ 10 - 7
data/conf/rspamd/custom/bad_asn.map

@@ -1,11 +1,12 @@
 # High spam networks, disabled by default
+# ASN:SCORE DESC
+# Remove comment to enable score
 #201942:5 #Soltia Consulting SL - ipinfo.io
-#16276:5 #OVH
-#12876:5 #ONLINE S.A.S
-#31034:5
-#12874:5
-#30823:5
-#197071:5
+#16276:2 #OVH
+#12876:2 #ONLINE S.A.S
+#31034:5 #ARUBA-ASN, IT
+#12874:5 #FASTWEB, IT
+#30823:3 #PKV spam
 #42831:5 #UK Dedicated Servers Ltd
 #29119:5 #Aire Networks del Mediterraneo S.L.U.
 #13335:5 #Cloudflare
@@ -17,7 +18,7 @@
 #14061:4 #Digitalocean
 #55293:4 #A2 Hosting
 #63018:4 #US Dedicated
-#197518:2
+#197518:2 #RACKMARKT
 #44493:2
 #46606:2
 #49505:2
@@ -25,3 +26,5 @@
 #197695:2
 #198068:2
 #43146:2
+#49100:4
+#39364:4

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

@@ -9,7 +9,8 @@ require_once "vars.inc.php";
 
 ini_set('error_reporting', 0);
 
-$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $opt = [
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
     PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

+ 0 - 15
data/conf/rspamd/local.d/multimap.conf

@@ -25,13 +25,6 @@ WHITELISTED_FWD_HOST {
   symbols_set = ["WHITELISTED_FWD_HOST"];
 }
 
-KEEP_SPAM {
-  type = "ip";
-  map = "redis://KEEP_SPAM";
-  action = "accept";
-  symbols_set = ["KEEP_SPAM"];
-}
-
 LOCAL_BL_ASN {
   require_symbols = "!MAILCOW_WHITE";
   type = "asn";
@@ -40,11 +33,3 @@ LOCAL_BL_ASN {
   description = "Sender's ASN is on the local blacklist";
   symbols_set = ["LOCAL_BL_ASN"];
 }
-
-#SPOOFED_SENDER {
-#  type = "rcpt";
-#  filter = "email:domain:tld";
-#  map = "redis://DOMAIN_MAP";
-#  require_symbols = "AUTH_NA | !RCVD_VIA_SMTP_AUTH";
-#  symbols_set = ["SPOOFED_SENDER"];
-#}

+ 62 - 0
data/conf/rspamd/lua/rspamd.local.lua

@@ -7,6 +7,68 @@ rspamd_config.MAILCOW_AUTH = {
 	end
 }
 
+rspamd_config:register_symbol({
+  name = 'KEEP_SPAM',
+  type = 'prefilter',
+  callback = function(task)
+    local util = require("rspamd_util")
+    local rspamd_logger = require "rspamd_logger"
+    local rspamd_ip = require 'rspamd_ip'
+    local uname = task:get_user()
+
+    if uname then
+      return false
+    end
+
+    local redis_params = rspamd_parse_redis_server('keep_spam')
+    local ip = task:get_from_ip()
+
+    if not ip:is_valid() then
+      return false
+    end
+
+    local from_ip_string = tostring(ip)
+    ip_check_table = {from_ip_string}
+
+    local maxbits = 128
+    local minbits = 32
+    if ip:get_version() == 4 then
+        maxbits = 32
+        minbits = 8
+    end
+    for i=maxbits,minbits,-1 do
+      local nip = ip:apply_mask(i):to_string() .. "/" .. i
+      table.insert(ip_check_table, nip)
+    end
+    local function keep_spam_cb(err, data)
+      if err then
+        rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
+        return false
+      else
+        for k,v in pairs(data) do
+          if (v and v ~= userdata and v == '1') then
+            rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result", v)
+            task:set_pre_result('accept', 'IP matched with forward hosts')
+          end
+        end
+      end
+    end
+    table.insert(ip_check_table, 1, 'KEEP_SPAM')
+    local redis_ret_user = rspamd_redis_make_request(task,
+      redis_params, -- connect params
+      'KEEP_SPAM', -- hash key
+      false, -- is write
+      keep_spam_cb, --callback
+      'HMGET', -- command
+      ip_check_table -- arguments
+    )
+    if not redis_ret_user then
+      rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map")
+    end
+  end,
+  priority = 19
+})
+
 rspamd_config:register_symbol({
   name = 'TAG_MOO',
   type = 'postfilter',

+ 2 - 1
data/conf/rspamd/meta_exporter/pipe.php

@@ -6,7 +6,8 @@ require_once "vars.inc.php";
 // Do not show errors, we log to using error_log
 ini_set('error_reporting', 0);
 // Init database
-$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $opt = [
     PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
     PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

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

@@ -1,7 +1,7 @@
 bind_socket = "*:11334";
-count = 2;
+count = 1;
 secure_ip = "127.0.0.1";
 secure_ip = "::1";
-bind_socket = "/rspamd-sock/rspamd.sock mode=0666 owner=nobody";
+bind_socket = "/var/lib/rspamd/rspamd.sock mode=0666 owner=nobody";
 .include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc"
 .include(try=true; priority=20) "$CONFDIR/override.d/worker-controller.custom.inc" 

+ 6 - 6
data/conf/sogo/sogo.conf

@@ -39,17 +39,17 @@
 
     SxVMemLimit = 384;
 
-    SOGoMaximumPingInterval = 354;
+    SOGoMaximumPingInterval = 3540;
 
-    SOGoInternalSyncInterval = 30;
-    SOGoMaximumSyncInterval = 354;
+    SOGoInternalSyncInterval = 45;
+    SOGoMaximumSyncInterval = 3540;
 
     // 100 seems to break some Android clients
-    //SOGoMaximumSyncWindowSize = 100;
+    //SOGoMaximumSyncWindowSize = 99;
     // This should do the trick for Outlook 2016
-    SOGoMaximumSyncResponseSize = 2048;
+    SOGoMaximumSyncResponseSize = 512;
 
-    WOWatchDogRequestTimeout = 10;
+    WOWatchDogRequestTimeout = 20;
     WOListenQueueSize = 300;
     WONoDetach = YES;
 

+ 10 - 1
data/conf/unbound/unbound.conf

@@ -2,7 +2,7 @@ server:
   verbosity: 1
   interface: 0.0.0.0
   interface: ::0
-  logfile: /dev/stdout
+  logfile: /dev/console
   do-ip4: yes
   do-ip6: yes
   do-udp: yes
@@ -27,3 +27,12 @@ server:
   hide-version: yes
   max-udp-size: 4096
   msg-buffer-size: 65552
+
+remote-control:
+    control-enable: yes
+    control-interface: 127.0.0.1
+    control-port: 8953
+    server-key-file: "/etc/unbound/unbound_server.key"
+    server-cert-file: "/etc/unbound/unbound_server.pem"
+    control-key-file: "/etc/unbound/unbound_control.key"
+    control-cert-file: "/etc/unbound/unbound_control.pem"

+ 61 - 62
data/web/admin.php

@@ -22,29 +22,29 @@ $tfa_data = get_tfa();
           <div class="form-group">
             <label class="control-label col-sm-3" for="admin_user"><?=$lang['admin']['admin'];?>:</label>
             <div class="col-sm-9">
-              <input type="text" class="form-control" name="admin_user" id="admin_user" value="<?=htmlspecialchars($admindetails['username']);?>" required>
+              <input type="text" class="form-control" name="admin_user" value="<?=htmlspecialchars($admindetails['username']);?>" required>
               &rdsh; <kbd>a-z A-Z - _ .</kbd>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-3" for="admin_pass"><?=$lang['admin']['password'];?>:</label>
             <div class="col-sm-9">
-            <input type="password" class="form-control" name="admin_pass" id="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
+            <input type="password" data-hibp="true" class="form-control" name="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-3" for="admin_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
             <div class="col-sm-9">
-            <input type="password" class="form-control" name="admin_pass2" id="admin_pass2">
+            <input type="password" class="form-control" name="admin_pass2">
             </div>
           </div>
           <div class="form-group">
             <div class="col-sm-offset-3 col-sm-9">
-              <button class="btn btn-default" id="edit_selected" data-id="admin" data-item="admin" data-api-url='edit/self' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+              <button class="btn btn-default" data-action="edit_selected" data-id="admin" data-item="admin" data-api-url='edit/self' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
             </div>
           </div>
         </form>
-        <hr>
+        <legend><?=$lang['tfa']['tfa'];?></legend>
         <div class="row">
           <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?>:</div>
           <div class="col-sm-9 col-xs-7">
@@ -76,12 +76,10 @@ $tfa_data = get_tfa();
             </select>
           </div>
         </div>
-      </div>
-    </div>
-
-    <div class="hidden panel panel-primary">
-      <div class="panel-heading">API</div>
-      <div class="panel-body">
+        <legend data-target="#api" style="margin-top:40px;cursor:pointer" id="api_legend" unselectable="on" data-toggle="collapse">
+          <span id="api_arrow" style="font-size:12px" class="rotate glyphicon glyphicon-menu-down"></span> API (experimental, work in progress)
+        </legend>
+        <div id="api" class="collapse">
         <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
           <div class="form-group">
             <label class="control-label col-sm-3" for="allow_from"><?=$lang['admin']['api_allow_from'];?>:</label>
@@ -111,6 +109,7 @@ $tfa_data = get_tfa();
             </div>
           </div>
         </form>
+        </div>
       </div>
     </div>
 
@@ -125,12 +124,12 @@ $tfa_data = get_tfa();
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="domain_admins" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
               <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
               <ul class="dropdown-menu">
-                <li><a id="edit_selected" data-id="domain_admins" data-api-url='edit/domain-admin' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                <li><a id="edit_selected" data-id="domain_admins" data-api-url='edit/domain-admin' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                <li><a data-action="edit_selected" data-id="domain_admins" data-api-url='edit/domain-admin' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                <li><a data-action="edit_selected" data-id="domain_admins" data-api-url='edit/domain-admin' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                 <li role="separator" class="divider"></li>
-                <li><a id="edit_selected" data-id="domain_admins" data-api-url='edit/domain-admin' data-api-attr='{"disable_tfa":"1"}' href="#"><?=$lang['tfa']['disable_tfa'];?></a></li>
+                <li><a data-action="edit_selected" data-id="domain_admins" data-api-url='edit/domain-admin' data-api-attr='{"disable_tfa":"1"}' href="#"><?=$lang['tfa']['disable_tfa'];?></a></li>
                 <li role="separator" class="divider"></li>
-                <li><a id="delete_selected" data-id="domain_admins" data-api-url='delete/domain-admin' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                <li><a data-action="delete_selected" data-id="domain_admins" data-api-url='delete/domain-admin' href="#"><?=$lang['mailbox']['remove'];?></a></li>
               </ul>
               <a class="btn btn-sm btn-success" data-id="add_domain_admin" data-toggle="modal" data-target="#addDomainAdminModal" href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add_domain_admin'];?></a>
             </div>
@@ -156,13 +155,13 @@ $tfa_data = get_tfa();
             <div class="form-group">
               <label class="control-label col-sm-3" for="rspamd_ui_pass"><?=$lang['admin']['password'];?>:</label>
               <div class="col-sm-9">
-              <input type="password" class="form-control" name="rspamd_ui_pass" id="rspamd_ui_pass" required>
+              <input type="password" class="form-control" name="rspamd_ui_pass" required>
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-3" for="rspamd_ui_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
               <div class="col-sm-9">
-              <input type="password" class="form-control" name="rspamd_ui_pass2" id="rspamd_ui_pass2" required>
+              <input type="password" class="form-control" name="rspamd_ui_pass2" required>
               </div>
             </div>
             <div class="form-group">
@@ -202,7 +201,7 @@ $tfa_data = get_tfa();
         <div class="mass-actions-admin">
           <div class="btn-group btn-group-sm">
             <button type="button" id="toggle_multi_select_all" data-id="dkim" class="btn btn-default"><?=$lang['mailbox']['toggle_all'];?></button>
-            <button type="button" id="delete_selected" name="delete_selected" data-id="dkim" data-api-url="delete/dkim" class="btn btn-danger"><?=$lang['admin']['remove'];?></button>
+            <button type="button" data-action="delete_selected" name="delete_selected" data-id="dkim" data-api-url="delete/dkim" class="btn btn-danger"><?=$lang['admin']['remove'];?></button>
           </div>
         </div>
         <?php
@@ -310,7 +309,7 @@ $tfa_data = get_tfa();
           </div>
           <div class="form-group">
             <label for="domain">Selector</label>
-            <input class="form-control input-sm" id="dkim_selector" name="dkim_selector" value="dkim" required>
+            <input class="form-control input-sm" name="dkim_selector" value="dkim" required>
           </div>
           <div class="form-group">
             <select data-width="200px" data-style="btn btn-default btn-sm" class="form-control" id="key_size" name="key_size" title="<?=$lang['admin']['dkim_key_length'];?>" required>
@@ -318,7 +317,7 @@ $tfa_data = get_tfa();
               <option data-subtext="bits">2048</option>
             </select>
           </div>
-          <button class="btn btn-sm btn-default" id="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
+          <button class="btn btn-sm btn-default" data-action="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
         </form>
 
         <legend data-target="#import_dkim" style="margin-top:40px;cursor:pointer" id="import_dkim_legend" unselectable="on" data-toggle="collapse">
@@ -328,17 +327,17 @@ $tfa_data = get_tfa();
         <form class="form" data-id="dkim_import" role="form" method="post">
           <div class="form-group">
             <label for="domain">Domain:</label>
-            <input class="form-control input-sm" id="domain" name="domain" placeholder="example.org" required>
+            <input class="form-control input-sm" name="domain" placeholder="example.org" required>
           </div>
           <div class="form-group">
             <label for="domain">Selector:</label>
-            <input class="form-control input-sm" id="dkim_selector" name="dkim_selector" value="dkim" required>
+            <input class="form-control input-sm" name="dkim_selector" value="dkim" required>
           </div>
           <div class="form-group">
             <label for="private_key_file"><?=$lang['admin']['private_key'];?>:</label>
             <textarea class="form-control input-sm" rows="10" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA KEY-----"></textarea>
           </div>
-          <button class="btn btn-sm btn-default" id="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button>
+          <button class="btn btn-sm btn-default" data-action="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button>
         </form>
         </div>
 
@@ -384,7 +383,7 @@ $tfa_data = get_tfa();
             </select>
             </div>
           </div>
-          <button class="btn btn-sm btn-default" id="add_item" data-id="dkim_duplicate" data-api-url='add/dkim_duplicate' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-duplicate"></span> <?=$lang['admin']['duplicate'];?></button>
+          <button class="btn btn-sm btn-default" data-action="add_item" data-id="dkim_duplicate" data-api-url='add/dkim_duplicate' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-duplicate"></span> <?=$lang['admin']['duplicate'];?></button>
         </form>
         </div>
 
@@ -404,10 +403,10 @@ $tfa_data = get_tfa();
             <button type="button" id="toggle_multi_select_all" data-id="fwdhosts" class="btn btn-default"><?=$lang['mailbox']['toggle_all'];?></button>
             <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
             <ul class="dropdown-menu">
-              <li><a id="edit_selected" data-id="fwdhosts" data-api-url='edit/fwdhost' data-api-attr='{"keep_spam":"0"}' href="#">Enable spam filter</a></li>
-              <li><a id="edit_selected" data-id="fwdhosts" data-api-url='edit/fwdhost' data-api-attr='{"keep_spam":"1"}' href="#">Disable spam filter</a></li>
+              <li><a data-action="edit_selected" data-id="fwdhosts" data-api-url='edit/fwdhost' data-api-attr='{"keep_spam":"0"}' href="#">Enable spam filter</a></li>
+              <li><a data-action="edit_selected" data-id="fwdhosts" data-api-url='edit/fwdhost' data-api-attr='{"keep_spam":"1"}' href="#">Disable spam filter</a></li>
               <li role="separator" class="divider"></li>
-              <li><a id="delete_selected" data-id="fwdhosts" data-api-url='delete/fwdhost' href="#"><?=$lang['admin']['remove'];?></a></li>
+              <li><a data-action="delete_selected" data-id="fwdhosts" data-api-url='delete/fwdhost' href="#"><?=$lang['admin']['remove'];?></a></li>
             </ul>
           </div>
         </div>
@@ -416,7 +415,7 @@ $tfa_data = get_tfa();
         <form class="form" data-id="fwdhost" role="form" method="post">
           <div class="form-group">
             <label for="hostname"><?=$lang['admin']['host'];?></label>
-            <input class="form-control" id="hostname" name="hostname" placeholder="example.org" required>
+            <input class="form-control" name="hostname" placeholder="example.org" required>
           </div>
           <div class="form-group">
             <select data-width="200px" class="form-control" id="filter_spam" name="filter_spam" title="<?=$lang['user']['spamfilter'];?>" required>
@@ -424,7 +423,7 @@ $tfa_data = get_tfa();
               <option value="0"><?=$lang['admin']['inactive'];?></option>
             </select>
           </div>
-          <button class="btn btn-default" id="add_item" data-id="fwdhost" data-api-url='add/fwdhost' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
+          <button class="btn btn-default" data-action="add_item" data-id="fwdhost" data-api-url='add/fwdhost' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
         </form>
       </div>
     </div>
@@ -439,41 +438,41 @@ $tfa_data = get_tfa();
         <form class="form" data-id="f2b" role="form" method="post">
           <div class="form-group">
             <label for="ban_time"><?=$lang['admin']['f2b_ban_time'];?>:</label>
-            <input type="number" class="form-control" id="ban_time" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required>
+            <input type="number" class="form-control" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required>
           </div>
           <div class="form-group">
             <label for="max_attempts"><?=$lang['admin']['f2b_max_attempts'];?>:</label>
-            <input type="number" class="form-control" id="max_attempts" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required>
+            <input type="number" class="form-control" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required>
           </div>
           <div class="form-group">
             <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>
+            <input type="number" class="form-control" 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>
+              <input type="number" class="form-control" 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>
+              <input type="number" class="form-control" name="netban_ipv6" value="<?=$f2b_data['netban_ipv6'];?>" required>
             </div>
           </div>
           <p class="help-block"><?=$lang['admin']['f2b_list_info'];?></p>
           <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>
+            <textarea class="form-control" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
           </div>
           <div class="form-group">
             <label for="blacklist"><?=$lang['admin']['f2b_blacklist'];?>:</label>
-            <textarea class="form-control" id="blacklist" name="blacklist" rows="5"><?=$f2b_data['blacklist'];?></textarea>
+            <textarea class="form-control" name="blacklist" rows="5"><?=$f2b_data['blacklist'];?></textarea>
           </div>
           <div class="btn-group">
-            <button class="btn btn-default" id="edit_selected" data-item="self" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+            <button class="btn btn-default" data-action="edit_selected" data-item="self" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
             <a href="#" role="button" class="btn btn-default" data-toggle="modal" data-container="netfilter-mailcow" data-target="#RestartContainer"><span class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_netfilter']; ?></a>
           </div>
         </form>
@@ -491,9 +490,9 @@ $tfa_data = get_tfa();
           <?php
           if ($active_bans['queued_for_unban'] == 0):
           ?>
-          <a id="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[<?=$lang['admin']['queue_unban'];?>]</a>
-          <a id="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
-          <a id="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist]</a>
+          <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[<?=$lang['admin']['queue_unban'];?>]</a>
+          <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
+          <a data-action="edit_selected" data-item="<?=$active_bans['network'];?>" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist]</a>
           <?php
           else:
           ?>
@@ -528,10 +527,10 @@ $tfa_data = get_tfa();
             <button type="button" id="toggle_multi_select_all" data-id="rlyhosts" class="btn btn-default"><?=$lang['mailbox']['toggle_all'];?></button>
             <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
             <ul class="dropdown-menu">
-              <li><a id="edit_selected" data-id="rlyhosts" data-api-url='edit/relayhost' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-              <li><a id="edit_selected" data-id="rlyhosts" data-api-url='edit/relayhost' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+              <li><a data-action="edit_selected" data-id="rlyhosts" data-api-url='edit/relayhost' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+              <li><a data-action="edit_selected" data-id="rlyhosts" data-api-url='edit/relayhost' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
               <li role="separator" class="divider"></li>
-              <li><a id="delete_selected" data-id="rlyhosts" data-api-url='delete/relayhost' href="#"><?=$lang['admin']['remove'];?></a></li>
+              <li><a data-action="delete_selected" data-id="rlyhosts" data-api-url='delete/relayhost' href="#"><?=$lang['admin']['remove'];?></a></li>
             </ul>
           </div>
         </div>
@@ -540,17 +539,17 @@ $tfa_data = get_tfa();
         <form class="form" data-id="rlyhost" role="form" method="post">
           <div class="form-group">
             <label for="hostname"><?=$lang['admin']['host'];?></label>
-            <input class="form-control" id="hostname" name="hostname" required>
+            <input class="form-control" name="hostname" required>
           </div>
           <div class="form-group">
             <label for="hostname"><?=$lang['admin']['username'];?></label>
-            <input class="form-control" id="username" name="username">
+            <input class="form-control" name="username">
           </div>
           <div class="form-group">
             <label for="hostname"><?=$lang['admin']['password'];?></label>
-            <input class="form-control" id="password" name="password">
+            <input class="form-control" name="password">
           </div>
-          <button class="btn btn-default" id="add_item" data-id="rlyhost" data-api-url='add/relayhost' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
+          <button class="btn btn-default" data-action="add_item" data-id="rlyhost" data-api-url='add/relayhost' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
         </form>
       </div>
     </div>
@@ -565,19 +564,19 @@ $tfa_data = get_tfa();
             <div class="col-sm-6">
               <div class="form-group">
                 <label for="retention_size"><?=$lang['admin']['quarantine_retention_size'];?></label>
-                <input type="number" class="form-control" id="retention_size" name="retention_size" value="<?=$q_data['retention_size'];?>" placeholder="0" required>
+                <input type="number" class="form-control" name="retention_size" value="<?=$q_data['retention_size'];?>" placeholder="0" required>
               </div>
             </div>
             <div class="col-sm-6">
               <div class="form-group">
                 <label for="max_size"><?=$lang['admin']['quarantine_max_size'];?></label>
-                <input type="number" class="form-control" id="max_size" name="max_size" value="<?=$q_data['max_size'];?>" placeholder="0" required>
+                <input type="number" class="form-control" name="max_size" value="<?=$q_data['max_size'];?>" placeholder="0" required>
               </div>
             </div>
           </div>
           <div class="form-group">
             <label for="exclude_domains"><?=$lang['admin']['quarantine_exclude_domains'];?></label><br />
-            <select data-width="100%" id="exclude_domains" name="exclude_domains" class="selectpicker" title="<?=$lang['tfa']['select'];?>" multiple>
+            <select data-width="100%" name="exclude_domains" class="selectpicker" title="<?=$lang['tfa']['select'];?>" multiple>
               <?php
               foreach (array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')) as $domain):
               ?>
@@ -587,7 +586,7 @@ $tfa_data = get_tfa();
               ?>
             </select>
           </div>
-          <button class="btn btn-default" id="edit_selected" data-item="self" data-id="quarantine" data-api-url='edit/quarantine' data-api-attr='{"action":"settings"}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+          <button class="btn btn-default" data-action="edit_selected" data-item="self" data-id="quarantine" data-api-url='edit/quarantine' data-api-attr='{"action":"settings"}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
         </form>
       </div>
     </div>
@@ -649,19 +648,19 @@ $tfa_data = get_tfa();
                     <input type="hidden" name="active" value="0">
                     <div class="form-group">
                       <label for="desc"><?=$lang['admin']['rsetting_desc'];?>:</label>
-                      <input type="text" class="form-control" id="desc" name="desc" value="<?=$rsetting_details['desc'];?>">
+                      <input type="text" class="form-control" name="desc" value="<?=$rsetting_details['desc'];?>">
                     </div>
                     <div class="form-group">
                       <label for="content"><?=$lang['admin']['rsetting_content'];?>:</label>
-                      <textarea class="form-control" id="content" name="content" rows="10"><?=$rsetting_details['content'];?></textarea>
+                      <textarea class="form-control" name="content" rows="10"><?=$rsetting_details['content'];?></textarea>
                     </div>
                     <div class="form-group">
                       <label>
                         <input type="checkbox" name="active" value="1" <?=($rsetting_details['active_int'] == 1) ? 'checked' : null;?>> <?=$lang['admin']['active'];?>
                       </label>
                     </div>
-                    <button class="btn btn-default" id="edit_selected" data-item="<?=$rsetting_details['id'];?>" data-id="rsettings" data-api-url='edit/rsetting' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
-                    <button class="btn btn-danger" id="delete_selected" data-item="<?=$rsetting_details['id'];?>" data-id="rsettings" data-api-url="delete/rsetting" data-api-attr='{}' href="#"><?=$lang['admin']['remove'];?></button>
+                    <button class="btn btn-default" data-action="edit_selected" data-item="<?=$rsetting_details['id'];?>" data-id="rsettings" data-api-url='edit/rsetting' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+                    <button class="btn btn-danger" data-action="delete_selected" data-item="<?=$rsetting_details['id'];?>" data-id="rsettings" data-api-url="delete/rsetting" data-api-attr='{}' href="#"><?=$lang['admin']['remove'];?></button>
                   </form>
                 </div>
                 <?php
@@ -744,7 +743,7 @@ $tfa_data = get_tfa();
             ?>
           </table>
           <p><div class="btn-group">
-            <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" data-action="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>
@@ -755,21 +754,21 @@ $tfa_data = get_tfa();
         <form class="form" data-id="uitexts" role="form" method="post">
           <div class="form-group">
             <label for="title_name"><?=$lang['admin']['title_name'];?>:</label>
-            <input type="text" class="form-control" id="title_name" name="title_name" placeholder="mailcow UI" value="<?=$ui_texts['title_name'];?>">
+            <input type="text" class="form-control" name="title_name" placeholder="mailcow UI" value="<?=$ui_texts['title_name'];?>">
           </div>
           <div class="form-group">
             <label for="main_name"><?=$lang['admin']['main_name'];?>:</label>
-            <input type="text" class="form-control" id="main_name" name="main_name" placeholder="mailcow UI" value="<?=$ui_texts['main_name'];?>">
+            <input type="text" class="form-control" name="main_name" placeholder="mailcow UI" value="<?=$ui_texts['main_name'];?>">
           </div>
           <div class="form-group">
             <label for="apps_name"><?=$lang['admin']['apps_name'];?>:</label>
-            <input type="text" class="form-control" id="apps_name" name="apps_name" placeholder="mailcow Apps" value="<?=$ui_texts['apps_name'];?>">
+            <input type="text" class="form-control" name="apps_name" placeholder="mailcow Apps" value="<?=$ui_texts['apps_name'];?>">
           </div>
           <div class="form-group">
             <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-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>
+          <button class="btn btn-default" data-action="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>
@@ -791,8 +790,8 @@ echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n";
 ?>
 </script>
-<script src="js/footable.min.js"></script>
-<script src="js/admin.js"></script>
+<script src="/js/footable.min.js"></script>
+<script src="/js/admin.js"></script>
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
 } else {

+ 2 - 1
data/web/autodiscover.php

@@ -27,7 +27,8 @@ if (strpos($data, 'autodiscover/outlook/responseschema') !== false) {
   }
 }
 
-$dsn = $database_type . ":host=" . $database_host . ";dbname=" . $database_name;
+//$dsn = $database_type . ":host=" . $database_host . ";dbname=" . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $opt = [
   PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
   PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

+ 0 - 6
data/web/css/admin.css

@@ -65,12 +65,6 @@ body.modal-open {
   font-size:9pt;
   background:transparent;
 }
-.bootstrap-select {
-  width: auto!important;
-}
 .table-condensed .input-sm {
   width: 100%!important;  
 }
-.full-width-select {
-  width: 100%!important;  
-}

+ 0 - 3
data/web/css/mailbox.css

@@ -5,9 +5,6 @@ table.footable>tbody>tr.footable-empty>td {
 .pagination a {
   text-decoration: none !important;
 }
-.panel panel-default {
-  overflow: visible !important;
-}
 .btn-group {
   width: max-content;
 }

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

@@ -148,3 +148,13 @@ nav .glyphicon {
   color: #5a5a5a;
   white-space: nowrap;
 }
+.haveibeenpwned {
+  cursor: pointer;
+  -webkit-user-select: none;  
+  -moz-user-select: none;    
+  -ms-user-select: none;      
+  user-select: none;
+}
+.full-width-select {
+  width: 100%!important;  
+}

+ 2 - 2
data/web/debug.php

@@ -288,8 +288,8 @@ echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n";
 
 ?>
 </script>
-<script src="js/footable.min.js"></script>
-<script src="js/debug.js"></script>
+<script src="/js/footable.min.js"></script>
+<script src="/js/debug.js"></script>
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
 }

+ 169 - 58
data/web/edit.php

@@ -39,13 +39,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
                 <div class="col-sm-10">
                   <textarea id="textarea_alias_goto" class="form-control" autocapitalize="none" autocorrect="off" rows="10" id="goto" name="goto" required><?= (!preg_match('/^(null|ham|spam)@localhost$/i', $result['goto'])) ? htmlspecialchars($result['goto']) : null; ?></textarea>
                   <div class="checkbox">
-                    <label><input class="goto_checkbox"id="goto_null" type="checkbox" value="1" name="goto_null" <?= ($result['goto'] == "null@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_null'];?></label>
+                    <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_null" <?= ($result['goto'] == "null@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_null'];?></label>
                   </div>
                   <div class="checkbox">
-                    <label><input class="goto_checkbox" id="goto_spam" type="checkbox" value="1" name="goto_spam" <?= ($result['goto'] == "spam@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_spam'];?></label>
+                    <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_spam" <?= ($result['goto'] == "spam@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_spam'];?></label>
                   </div>
                   <div class="checkbox">
-                    <label><input class="goto_checkbox" id="goto_ham" type="checkbox" value="1" name="goto_ham" <?= ($result['goto'] == "ham@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_ham'];?></label>
+                    <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_ham" <?= ($result['goto'] == "ham@localhost") ? "checked" : null; ?>> <?=$lang['add']['goto_ham'];?></label>
                   </div>
                 </div>
               </div>
@@ -58,7 +58,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               </div>
               <div class="form-group">
                 <div class="col-sm-offset-2 col-sm-10">
-                  <button class="btn btn-success" id="edit_selected" data-id="editalias" data-item="<?=htmlspecialchars($alias);?>" data-api-url='edit/alias' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                  <button class="btn btn-success" data-action="edit_selected" data-id="editalias" data-item="<?=htmlspecialchars($alias);?>" data-api-url='edit/alias' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
                 </div>
               </div>
             </form>
@@ -92,7 +92,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="domains"><?=$lang['edit']['domains'];?></label>
               <div class="col-sm-10">
-                <select data-live-search="true" id="domains" name="domains" multiple required>
+                <select data-live-search="true" class="full-width-select" name="domains" multiple required>
                 <?php
                 foreach ($result['selected_domains'] as $domain):
                 ?>
@@ -111,13 +111,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
               <div class="col-sm-10">
-              <input type="password" class="form-control" name="password" id="password" placeholder="">
+              <input type="password" data-hibp="true" class="form-control" name="password" placeholder="">
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
               <div class="col-sm-10">
-              <input type="password" class="form-control" name="password2" id="password2">
+              <input type="password" class="form-control" name="password2">
               </div>
             </div>
             <div class="form-group">
@@ -136,7 +136,31 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editdomainadmin" data-item="<?=$domain_admin;?>" data-api-url='edit/domain-admin' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editdomainadmin" data-item="<?=$domain_admin;?>" data-api-url='edit/domain-admin' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+              </div>
+            </div>
+          </form>
+          <form data-id="daacl" class="form-inline well" method="post">
+            <div class="row">
+              <div class="col-sm-1">
+                <p class="help-block">ACL</p>
+              </div>
+              <div class="col-sm-10">
+                <div class="form-group">
+                  <select id="da_acl" name="da_acl" size="10" multiple>
+                  <?php
+                  $da_acls = acl('get', 'domainadmin', $domain_admin);
+                  foreach ($da_acls as $acl => $val):
+                    ?>
+                    <option value="<?=$acl;?>" <?=($val == 1) ? 'selected' : null;?>><?=$lang['acl'][$acl];?></option>
+                    <?php
+                  endforeach;
+                  ?>
+                  </select>
+                </div>
+                <div class="form-group">
+                  <button class="btn btn-default" data-action="edit_selected" data-id="daacl" data-item="<?=htmlspecialchars($domain_admin);?>" data-api-url='edit/da-acl' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+                </div>
               </div>
             </div>
           </form>
@@ -165,7 +189,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="description"><?=$lang['edit']['description'];?></label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="description" id="description" value="<?=htmlspecialchars($result['description']);?>">
+                <input type="text" class="form-control" name="description" value="<?=htmlspecialchars($result['description']);?>">
               </div>
             </div>
             <?php
@@ -174,31 +198,31 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="aliases"><?=$lang['edit']['max_aliases'];?></label>
               <div class="col-sm-10">
-                <input type="number" class="form-control" name="aliases" id="aliases" value="<?=intval($result['max_num_aliases_for_domain']);?>">
+                <input type="number" class="form-control" name="aliases" value="<?=intval($result['max_num_aliases_for_domain']);?>">
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="mailboxes"><?=$lang['edit']['max_mailboxes'];?></label>
               <div class="col-sm-10">
-                <input type="number" class="form-control" name="mailboxes" id="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
+                <input type="number" class="form-control" name="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="maxquota"><?=$lang['edit']['max_quota'];?></label>
               <div class="col-sm-10">
-                <input type="number" class="form-control" name="maxquota" id="maxquota" value="<?=intval($result['max_quota_for_mbox'] / 1048576);?>">
+                <input type="number" class="form-control" name="maxquota" value="<?=intval($result['max_quota_for_mbox'] / 1048576);?>">
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['domain_quota'];?></label>
               <div class="col-sm-10">
-                <input type="number" class="form-control" name="quota" id="quota" value="<?=intval($result['max_quota_for_domain'] / 1048576);?>">
+                <input type="number" class="form-control" name="quota" value="<?=intval($result['max_quota_for_domain'] / 1048576);?>">
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="quota">Relayhost</label>
               <div class="col-sm-10">
-                <select data-live-search="true" name="relayhost" id="relayhost" class="form-control">
+                <select data-live-search="true" name="relayhost" class="form-control">
                   <?php
                   foreach ($rlyhosts as $rlyhost) {
                   ?>
@@ -233,7 +257,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editdomain" data-item="<?=$domain;?>" data-api-url='edit/domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editdomain" data-item="<?=$domain;?>" data-api-url='edit/domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
               </div>
             </div>
           </form>
@@ -256,17 +280,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
       <form data-id="domratelimit" class="form-inline well" method="post">
         <div class="form-group">
           <label class="control-label">Ratelimit</label>
-          <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
+          <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" autocomplete="off" class="form-control" placeholder="disabled">
         </div>
         <div class="form-group">
-          <select name="rl_frame" id="rl_frame" class="form-control">
+          <select name="rl_frame" class="form-control">
             <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
             <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
             <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
           </select>
         </div>
         <div class="form-group">
-          <button class="btn btn-default" id="edit_selected" data-id="domratelimit" data-item="<?=$domain;?>" data-api-url='edit/rl-domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+          <button data-acl="<?=$_SESSION['acl']['ratelimit'];?>" class="btn btn-default" data-action="edit_selected" data-id="domratelimit" data-item="<?=$domain;?>" data-api-url='edit/rl-domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
         </div>
       </form>
       <hr>
@@ -278,17 +302,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <table class="table table-striped table-condensed" id="wl_policy_domain_table"></table>
           </div>
           <div class="mass-actions-user">
-            <div class="btn-group">
+            <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
-              <a class="btn btn-sm btn-danger" id="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+              <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
               </ul>
             </div>
           </div>
           <form class="form-inline" data-id="add_wl_policy_domain">
-            <div class="input-group">
-              <input type="text" class="form-control" name="object_from" id="object_from" placeholder="*@example.org" required>
+            <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
+              <input type="text" class="form-control" name="object_from" placeholder="*@example.org" required>
               <span class="input-group-btn">
-                <button class="btn btn-default" id="add_item" data-id="add_wl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"wl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
+                <button class="btn btn-default" data-action="add_item" data-id="add_wl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"wl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
               </span>
             </div>
           </form>
@@ -300,17 +324,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <table class="table table-striped table-condensed" id="bl_policy_domain_table"></table>
           </div>
           <div class="mass-actions-user">
-            <div class="btn-group">
+            <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
-              <a class="btn btn-sm btn-danger" id="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+              <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
               </ul>
             </div>
           </div>
           <form class="form-inline" data-id="add_bl_policy_domain">
-            <div class="input-group">
-              <input type="text" class="form-control" name="object_from" id="object_from" placeholder="*@example.org" required>
+            <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
+              <input type="text" class="form-control" name="object_from" placeholder="*@example.org" required>
               <span class="input-group-btn">
-                <button class="btn btn-default" id="add_item" data-id="add_bl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"bl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
+                <button class="btn btn-default" data-action="add_item" data-id="add_bl_policy_domain" data-api-url='add/domain-policy' data-api-attr='{"domain":"<?= $domain; ?>","object_list":"bl"}' href="#"><?=$lang['user']['spamfilter_table_add'];?></button>
               </span>
             </div>
           </form>
@@ -338,7 +362,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="target_domain"><?=$lang['edit']['target_domain'];?></label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="target_domain" id="target_domain" value="<?=htmlspecialchars($result['target_domain']);?>">
+                <input type="text" class="form-control" name="target_domain" value="<?=htmlspecialchars($result['target_domain']);?>">
               </div>
             </div>
             <div class="form-group">
@@ -350,7 +374,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editaliasdomain" data-item="<?=$alias_domain;?>" data-api-url='edit/alias-domain' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editaliasdomain" data-item="<?=$alias_domain;?>" data-api-url='edit/alias-domain' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -358,17 +382,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <form data-id="domratelimit" class="form-inline well" method="post">
             <div class="form-group">
               <label class="control-label">Ratelimit</label>
-              <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
+              <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" autocomplete="off" class="form-control" placeholder="disabled">
             </div>
             <div class="form-group">
-              <select name="rl_frame" id="rl_frame" class="form-control">
+              <select name="rl_frame" class="form-control">
                 <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
                 <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
                 <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
               </select>
             </div>
             <div class="form-group">
-              <button class="btn btn-default" id="edit_selected" data-id="domratelimit" data-item="<?=$alias_domain;?>" data-api-url='edit/rl-domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+              <button class="btn btn-default" data-action="edit_selected" data-id="domratelimit" data-item="<?=$alias_domain;?>" data-api-url='edit/rl-domain' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
             </div>
           </form>
           <?php
@@ -406,7 +430,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?>:</label>
             <div class="col-sm-10">
-            <input type="text" class="form-control" name="name" id="name" value="<?=htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8');?>">
+            <input type="text" class="form-control" name="name" value="<?=htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8');?>">
             </div>
           </div>
           <div class="form-group">
@@ -414,13 +438,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               <br /><span id="quotaBadge" class="badge">max. <?=intval($result['max_new_quota'] / 1048576)?> MiB</span>
             </label>
             <div class="col-sm-10">
-              <input type="number" name="quota" id="quota" id="destroyable" style="width:100%" min="1" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control">
+              <input type="number" name="quota" style="width:100%" min="1" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control">
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?>:</label>
             <div class="col-sm-10">
-              <select data-live-search="true" data-width="100%" style="width:100%" id="sender_acl" name="sender_acl" size="10" multiple>
+              <select data-live-search="true" data-width="100%" style="width:100%" id="editSelectSenderACL" name="sender_acl" size="10" multiple>
               <?php
               $sender_acl_handles = mailbox('get', 'sender_acl_handles', $mailbox);
 
@@ -474,13 +498,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
             <div class="col-sm-10">
-            <input type="password" class="form-control" name="password" id="password" placeholder="<?=$lang['edit']['unchanged_if_empty'];?>">
+            <input type="password" data-hibp="true" class="form-control" name="password" placeholder="<?=$lang['edit']['unchanged_if_empty'];?>">
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
             <div class="col-sm-10">
-            <input type="password" class="form-control" name="password2" id="password2">
+            <input type="password" class="form-control" name="password2">
             </div>
           </div>
           <div class="form-group">
@@ -500,7 +524,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           </div>
           <div class="form-group">
             <div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-success" id="edit_selected" data-id="editmailbox" data-item="<?=htmlspecialchars($result['username']);?>" data-api-url='edit/mailbox' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+              <button class="btn btn-success" data-action="edit_selected" data-id="editmailbox" data-item="<?=htmlspecialchars($result['username']);?>" data-api-url='edit/mailbox' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
             </div>
           </div>
         </form>
@@ -512,17 +536,41 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="col-sm-10">
               <div class="form-group">
-                <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
+                <input name="rl_value" type="number" autocomplete="off" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
               </div>
               <div class="form-group">
-                <select name="rl_frame" id="rl_frame" class="form-control">
+                <select name="rl_frame" class="form-control">
                   <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
                   <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
                   <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
                 </select>
               </div>
               <div class="form-group">
-                <button class="btn btn-default" id="edit_selected" data-id="mboxratelimit" data-item="<?=htmlspecialchars($mailbox);?>" data-api-url='edit/rl-mbox' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+                <button class="btn btn-default" data-action="edit_selected" data-id="mboxratelimit" data-item="<?=htmlspecialchars($mailbox);?>" data-api-url='edit/rl-mbox' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+              </div>
+            </div>
+          </div>
+        </form>
+        <form data-id="useracl" class="form-inline well" method="post">
+          <div class="row">
+            <div class="col-sm-1">
+              <p class="help-block">ACL</p>
+            </div>
+            <div class="col-sm-10">
+              <div class="form-group">
+                <select id="user_acl" name="user_acl" size="10" multiple>
+                <?php
+                $user_acls = acl('get', 'user', $mailbox);
+                foreach ($user_acls as $acl => $val):
+                  ?>
+                  <option value="<?=$acl;?>" <?=($val == 1) ? 'selected' : null;?>><?=$lang['acl'][$acl];?></option>
+                  <?php
+                endforeach;
+                ?>
+                </select>
+              </div>
+              <div class="form-group">
+                <button class="btn btn-default" data-action="edit_selected" data-id="useracl" data-item="<?=htmlspecialchars($mailbox);?>" data-api-url='edit/user-acl' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
               </div>
             </div>
           </div>
@@ -541,19 +589,19 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="hostname"><?=$lang['add']['hostname'];?></label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="hostname" id="hostname" value="<?=htmlspecialchars($result['hostname'], ENT_QUOTES, 'UTF-8');?>" required>
+                <input type="text" class="form-control" name="hostname" value="<?=htmlspecialchars($result['hostname'], ENT_QUOTES, 'UTF-8');?>" required>
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?></label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="username" id="username" value="<?=htmlspecialchars($result['username'], ENT_QUOTES, 'UTF-8');?>">
+                <input type="text" class="form-control" name="username" value="<?=htmlspecialchars($result['username'], ENT_QUOTES, 'UTF-8');?>">
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="password"><?=$lang['add']['password'];?></label>
               <div class="col-sm-10">
-                <input type="password" class="form-control" name="password" id="password" value="<?=htmlspecialchars($result['password'], ENT_QUOTES, 'UTF-8');?>">
+                <input type="password" data-hibp="true" class="form-control" name="password" value="<?=htmlspecialchars($result['password'], ENT_QUOTES, 'UTF-8');?>">
               </div>
             </div>
             <div class="form-group">
@@ -565,7 +613,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editrelayhost" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/relayhost' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editrelayhost" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/relayhost' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -588,13 +636,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="description" id="description" value="<?=htmlspecialchars($result['description'], ENT_QUOTES, 'UTF-8');?>" required>
+                <input type="text" class="form-control" name="description" value="<?=htmlspecialchars($result['description'], ENT_QUOTES, 'UTF-8');?>" required>
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="domain"><?=$lang['edit']['kind'];?>:</label>
               <div class="col-sm-10">
-                <select name="kind" id="kind" title="<?=$lang['edit']['select'];?>" required>
+                <select name="kind" title="<?=$lang['edit']['select'];?>" required>
                   <option value="location" <?=($result['kind'] == "location") ? "selected" : null;?>>Location</option>
                   <option value="group" <?=($result['kind'] == "group") ? "selected" : null;?>>Group</option>
                   <option value="thing" <?=($result['kind'] == "thing") ? "selected" : null;?>>Thing</option>
@@ -604,7 +652,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?>:</label>
               <div class="col-sm-10">
-                <select name="multiple_bookings_select" id="multiple_bookings_select" title="<?=$lang['add']['select'];?>" required>
+                <select name="multiple_bookings_select" id="editSelectMultipleBookings" title="<?=$lang['add']['select'];?>" required>
                   <option value="0" <?=($result['multiple_bookings'] == 0) ? "selected" : null;?>><?=$lang['mailbox']['booking_0'];?></option>
                   <option value="-1" <?=($result['multiple_bookings'] == -1) ? "selected" : null;?>><?=$lang['mailbox']['booking_lt0'];?></option>
                   <option value="custom" <?=($result['multiple_bookings'] >= 1) ? "selected" : null;?>><?=$lang['mailbox']['booking_custom'];?></option>
@@ -625,7 +673,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editresource" data-item="<?=htmlspecialchars($result['name']);?>" data-api-url='edit/resource' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editresource" data-item="<?=htmlspecialchars($result['name']);?>" data-api-url='edit/resource' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -671,7 +719,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editbcc" data-item="<?=$bcc;?>" data-api-url='edit/bcc' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editbcc" data-item="<?=$bcc;?>" data-api-url='edit/bcc' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -683,7 +731,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         <?php
         }
     }
-    elseif (isset($_GET['recipient_map']) && !empty($_GET["recipient_map"])) {
+    elseif (isset($_GET['recipient_map']) &&
+      !empty($_GET["recipient_map"]) &&
+      $_SESSION['mailcow_cc_role'] == "admin") {
         $map = intval($_GET["recipient_map"]);
         $result = recipient_map('details', $map);
         if (substr($result['recipient_map_old'], 0, 1) == '@') {
@@ -718,7 +768,68 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="edit_recipient_map" data-item="<?=$map;?>" data-api-url='edit/recipient_map' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="edit_recipient_map" data-item="<?=$map;?>" data-api-url='edit/recipient_map' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+              </div>
+            </div>
+          </form>
+        <?php
+        }
+        else {
+        ?>
+          <div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+        <?php
+        }
+    }
+    elseif (isset($_GET['tls_policy_map']) &&
+      !empty($_GET["tls_policy_map"]) &&
+      $_SESSION['mailcow_cc_role'] == "admin") {
+        $map = intval($_GET["tls_policy_map"]);
+        $result = tls_policy_maps('details', $map);
+        if (!empty($result)) {
+          ?>
+          <h4><?=$lang['mailbox']['tls_policy_maps']?>: <?=$result['dest'];?></h4>
+          <br />
+          <form class="form-horizontal" data-id="edit_tls_policy_maps" role="form" method="post">
+            <input type="hidden" value="0" name="active">
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="dest"><?=$lang['mailbox']['tls_map_dest'];?></label>
+              <div class="col-sm-10">
+                <input value="<?=$result['dest'];?>" type="text" class="form-control" name="dest" id="dest">
+                <small><?=$lang['mailbox']['tls_map_dest_info'];?></small>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="policy"><?=$lang['mailbox']['tls_map_policy'];?></label>
+              <div class="col-sm-10">
+              <select class="full-width-select" name="policy" required>
+                <option value="none" <?=($result['policy'] != 'none') ?: 'selected';?>>none</option>
+                <option value="may" <?=($result['policy'] != 'may') ?: 'selected';?>>may</option>
+                <option value="encrypt" <?=($result['policy'] != 'encrypt') ?: 'selected';?>>encrypt</option>
+                <option value="dane" <?=($result['policy'] != 'dane') ?: 'selected';?>>dane-only</option>
+                <option value="dane-only" <?=($result['policy'] != 'dane-only') ?: 'selected';?>>dane-only</option>
+                <option value="fingerprint" <?=($result['policy'] != 'fingerprint') ?: 'selected';?>>fingerprint</option>
+                <option value="verify" <?=($result['policy'] != 'verify') ?: 'selected';?>>verify</option>
+                <option value="secure" <?=($result['policy'] != 'secure') ?: 'selected';?>>secure</option>
+              </select>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="parameters"><?=$lang['mailbox']['tls_map_parameters'];?></label>
+              <div class="col-sm-10">
+                <input value="<?=$result['parameters'];?>" type="text" class="form-control" name="parameters" id="parameters">
+                <small><?=$lang['mailbox']['tls_map_parameters_info'];?></small>
+              </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="active" <?php if (isset($result['active_int']) && $result['active_int']=="1") { echo "checked"; }; ?>> <?=$lang['edit']['active'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <button class="btn btn-success" data-action="edit_selected" data-id="edit_tls_policy_maps" data-item="<?=$map;?>" data-api-url='edit/tls-policy-map' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -885,7 +996,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editsyncjob" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/syncjob' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editsyncjob" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/syncjob' data-api-attr='{}' href="#"><?=$lang['edit']['save'];?></button>
               </div>
             </div>
           </form>
@@ -936,7 +1047,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-success" id="edit_selected" data-id="editfilter" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/filter' data-api-attr='{}' href="#"><?=$lang['edit']['validate_save'];?></button>
+                <button class="btn btn-success" data-action="edit_selected" data-id="editfilter" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/filter' data-api-attr='{}' href="#"><?=$lang['edit']['validate_save'];?></button>
               </div>
             </div>
           </form>
@@ -965,14 +1076,14 @@ else {
 <script type='text/javascript'>
 <?php
 $lang_user = json_encode($lang['user']);
-echo "var lang = ". $lang_user . ";\n";
+echo "var lang_user = ". $lang_user . ";\n";
 echo "var table_for_domain = '". ((isset($domain)) ? $domain : null) . "';\n";
 echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
 echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 ?>
 </script>
-<script src="js/footable.min.js"></script>
-<script src="js/edit.js"></script>
+<script src="/js/footable.min.js"></script>
+<script src="/js/edit.js"></script>
 <?php
 require_once("inc/footer.inc.php");
 ?>

+ 153 - 163
data/web/img/cow_mailcow.svg

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 
 <svg
    xmlns:dc="http://purl.org/dc/elements/1.1/"
@@ -10,20 +10,21 @@
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    version="1.1"
-   id="Layer_1"
+   id="layer1"
    x="0px"
    y="0px"
-   width="374.82101"
-   height="356.871"
-   viewBox="0 0 374.821 356.871"
-   enable-background="new 0 0 1600 1200"
+   width="434.82101"
+   height="376.871"
+   viewBox="0 0 434.82101 376.871"
+   enable-background="new 0 0 374.82 356.871"
    xml:space="preserve"
    inkscape:version="0.91 r13725"
    sodipodi:docname="cow_mailcow.svg"><metadata
-     id="metadata144"><rdf:RDF><cc:Work
+     id="metadata77"><rdf:RDF><cc:Work
          rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
-     id="defs142" /><sodipodi:namedview
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title><cc:license
+           rdf:resource="" /></cc:Work></rdf:RDF></metadata><defs
+     id="defs75" /><sodipodi:namedview
      pagecolor="#ffffff"
      bordercolor="#666666"
      borderopacity="1"
@@ -32,161 +33,150 @@
      guidetolerance="10"
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
-     inkscape:window-width="1097"
-     inkscape:window-height="1138"
-     id="namedview140"
+     inkscape:window-width="1721"
+     inkscape:window-height="1177"
+     id="namedview73"
      showgrid="false"
-     inkscape:zoom="1.5733334"
-     inkscape:cx="264.82839"
-     inkscape:cy="340.43592"
-     inkscape:window-x="814"
-     inkscape:window-y="0"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="g3"
-     fit-margin-top="0"
-     fit-margin-left="0"
-     fit-margin-right="0"
-     fit-margin-bottom="0" /><g
+     inkscape:zoom="1.4142136"
+     inkscape:cx="219.01206"
+     inkscape:cy="236.74714"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     fit-margin-top="10"
+     fit-margin-left="50"
+     fit-margin-bottom="10"
+     fit-margin-right="10"
+     showguides="true" /><g
      id="g3"
-     transform="translate(-648.292,-401.988)"><g
-       id="g5"><g
-         id="g7"><g
-           id="email"><path
-             d="m 890.306,557.81 29.26,11.373 0,172.027 c 0,9.753 -7.895,17.649 -17.638,17.649 l -235.998,0 c -9.743,0 -17.638,-7.896 -17.638,-17.649 l 0,-172.026 29.259,-8.937"
-             id="path10"
-             inkscape:connector-curvature="0"
-             style="fill:#5a3620" /><path
-             d="M 758.871,656.221 649.49,747.45 c 2.507,6.648 8.901,11.409 16.44,11.409 l 235.998,0 c 7.536,0 13.933,-4.761 16.444,-11.409 l -107.402,-91.229 -52.099,0 z"
-             id="path12"
-             inkscape:connector-curvature="0"
-             style="fill:#fee70f;fill-opacity:0.89499996" /><g
-             id="g14"><path
-               d="m 810.391,656.686 107.981,90.764 c -0.331,0.881 -0.744,1.726 -1.205,2.536 l 0.028,0.035 c 1.501,-2.596 2.371,-5.594 2.371,-8.81 l 0,-172.004 -109.175,87.479 z"
-               id="path16"
-               inkscape:connector-curvature="0"
-               style="fill:#f9e82d;fill-opacity:1" /><path
-               d="m 649.49,747.45 108.864,-90.764 -110.061,-87.479 0,172.003 c 0,3.216 0.876,6.214 2.367,8.81 l 0.039,-0.035 c -0.466,-0.809 -0.877,-1.654 -1.209,-2.535 z"
-               id="path18"
-               inkscape:connector-curvature="0"
-               style="fill:#f9e82d;fill-opacity:1" /></g></g><path
-           d="m 961.81,681.214 c 0,0 -15.232,16.783 -42.244,14.73 l 0,28.14 c 13.328,-5.185 47.061,-20.036 56.854,-40.809 l -14.61,-2.061 z"
-           id="path20"
-           inkscape:connector-curvature="0"
-           style="fill:#b58765" /><path
-           d="m 984.594,658.413 c 3.59,-9.156 7.701,-11 9.346,-11.346 -49.276,4.542 -32.99,38.693 -32.99,38.693 0,0 6.229,14.728 26.532,13.892 27.063,0.461 35.631,-50.166 35.631,-50.166 -6.654,11.655 -26.404,9.876 -38.519,8.927 z"
-           id="path22"
-           inkscape:connector-curvature="0"
-           style="fill:#fef3df" /><path
-           d="m 783.931,446.247 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 0,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.98,43.855 95.98,97.952 -10e-4,54.098 -42.973,97.953 -95.98,97.953 z"
-           id="path26"
-           inkscape:connector-curvature="0"
-           style="opacity:0.1;fill:#3d5263" /><g
-           id="g28"><g
-             id="g30"><polyline
-               points="691.144,492.5 673.257,540.276 686.55,605.582 712.496,631.852      "
-               id="polyline32"
-               style="fill:#3d5263" /><g
-               id="g34"><g
-                 id="g36"><polyline
-                   points="658.248,450.81 673.501,487.076 693.836,496.903 724.04,458.731        "
-                   id="polyline38"
-                   style="fill:#fef3df" /><g
-                   id="g40"><path
-                     d="m 710.634,473.205 c 0,0 -22.482,-25.556 -49.793,-18.975 0,0 4.667,34.118 46.349,44.019 l 2.61,8.533 c 0,0 -65.612,-9.689 -59.339,-67.593 0,0 49.008,-19.884 72.598,15.106"
-                     id="path42"
-                     inkscape:connector-curvature="0"
-                     style="fill:#b58765" /><polyline
-                     points="909.697,450.81 894.447,487.076 874.114,496.903 843.907,458.731         "
-                     id="polyline44"
-                     style="fill:#fef3df" /><path
-                     d="m 857.314,473.205 c 0,0 22.48,-25.556 49.79,-18.975 0,0 -4.664,34.118 -46.347,44.019 l -2.613,8.533 c 0,0 65.611,-9.689 59.339,-67.593 0,0 -49.006,-19.884 -72.6,15.106"
-                     id="path46"
-                     inkscape:connector-curvature="0"
-                     style="fill:#b58765" /></g></g><path
-                 d="m 726.619,647.067 55.945,0 16.40428,-204.81407 c -55.814,0 -112.41728,30.01707 -112.41728,77.85207 0,1.454 0.085,2.787 0.121,4.175 0.127,3.934 0.448,7.585 0.856,11.135 1.689,14.816 5.451,27.177 8.461,43.383 1.452,7.831 5.002,23.374 5.002,23.374 0.056,0.408 0.165,0.804 0.224,1.211 2.535,16.546 11.832,32.027 25.404,43.684 z"
-                 id="path48"
-                 inkscape:connector-curvature="0"
-                 style="fill:#b58765"
-                 sodipodi:nodetypes="cccscccccc" /><path
-                 d="m 781.992,433.489 0,213.577 55.944,0 c 13.572,-11.657 22.867,-27.138 25.406,-43.684 0.058,-0.407 0.163,-0.803 0.221,-1.211 0,0 3.549,-15.543 5.002,-23.374 3.011,-16.206 6.774,-28.567 8.464,-43.381 0.405,-3.552 0.724,-7.203 0.846,-11.137 0.042,-1.388 0.126,-2.721 0.126,-4.175 0,-47.834 -40.191,-86.615 -96.009,-86.615 z"
-                 id="path50"
-                 inkscape:connector-curvature="0"
-                 style="fill:#b58765" /><g
-                 id="g52"><g
-                   id="g54"><path
-                     d="m 860.944,613.502 c 0,28.321 -35.091,51.289 -78.383,51.289 -43.299,0 -78.388,-22.968 -78.388,-51.289 0,-28.325 35.089,-51.289 78.388,-51.289 43.292,0 78.383,22.964 78.383,51.289 z"
-                     id="path56"
-                     inkscape:connector-curvature="0"
-                     style="fill:#fef3df" /></g></g><g
-                 id="g58"><g
-                   id="g60"><g
-                     id="g62"><path
-                       d="m 747.044,605.582 c 0,6.215 -5.04,11.256 -11.261,11.256 -6.21,0 -11.253,-5.041 -11.253,-11.256 0,-6.223 5.043,-11.257 11.253,-11.257 6.22,0 11.261,5.034 11.261,11.257 z"
-                       id="path64"
-                       inkscape:connector-curvature="0"
-                       style="fill:#5a3620" /></g></g><g
-                   id="g66"><g
-                     id="g68"><path
-                       d="m 840.856,605.582 c 0,6.215 -5.037,11.256 -11.257,11.256 -6.218,0 -11.259,-5.041 -11.259,-11.256 0,-6.223 5.041,-11.257 11.259,-11.257 6.22,0 11.257,5.034 11.257,11.257 z"
-                       id="path70"
-                       inkscape:connector-curvature="0"
-                       style="fill:#5a3620" /></g></g></g><g
-                 id="g72"><path
-                   d="m 875.228,525.835 c 0.354,-3.113 0.634,-6.311 0.743,-9.754 0.035,-1.218 0.109,-2.384 0.109,-3.661 0,-40.785 -33.369,-74.043 -80.237,-75.775 l -7.335,0.005 -0.006,0 c -0.007,0.018 -25.93541,90.33209 79.27959,142.17809 3.15658,-17.35082 5.65276,-38.01947 7.44641,-52.99309 z"
-                   id="path74"
-                   inkscape:connector-curvature="0"
-                   style="fill:#87654a"
-                   sodipodi:nodetypes="ccsccccc" /></g><g
-                 id="g76"><g
-                   id="g78"><g
-                     id="g80"><g
-                       id="g82"><path
-                         d="m 843.907,519.681 c 0,6.964 -5.65,12.611 -12.618,12.611 -6.963,0 -12.614,-5.646 -12.614,-12.611 0,-6.97 5.651,-12.614 12.614,-12.614 6.968,0 12.618,5.644 12.618,12.614 z"
-                         id="path84"
-                         inkscape:connector-curvature="0"
-                         style="fill:#5a3620" /></g></g></g><g
-                   id="g86"><g
-                     id="g88"><g
-                       id="g90"><path
-                         d="m 752.028,519.681 c 0,6.964 -5.649,12.611 -12.612,12.611 -6.969,0 -12.612,-5.646 -12.612,-12.611 0,-6.97 5.642,-12.614 12.612,-12.614 6.964,0 12.612,5.644 12.612,12.614 z"
-                         id="path92"
-                         inkscape:connector-curvature="0"
-                         style="fill:#5a3620" /></g></g></g><g
-                   id="g94"><g
-                     id="g96"><path
-                       d="m 748.75,515.894 c 0,2.558 -2.071,4.629 -4.63,4.629 -2.558,0 -4.633,-2.072 -4.633,-4.629 0,-2.552 2.076,-4.626 4.633,-4.626 2.559,0 4.63,2.073 4.63,4.626 z"
-                       id="path98"
-                       inkscape:connector-curvature="0"
-                       style="fill:#ffffff" /></g></g><g
-                   id="g100"><g
-                     id="g102"><path
-                       d="m 839.771,515.894 c 0,2.558 -2.073,4.629 -4.629,4.629 -2.558,0 -4.631,-2.072 -4.631,-4.629 0,-2.552 2.072,-4.626 4.631,-4.626 2.555,0 4.629,2.073 4.629,4.626 z"
-                       id="path104"
-                       inkscape:connector-curvature="0"
-                       style="fill:#ffffff" /></g></g></g></g><path
-               d="m 734.557,443.625 c 0,0 -18.236,-25.199 0,-41.637 0,0 13.125,32.012 40.242,31.502"
-               id="path106"
-               inkscape:connector-curvature="0"
-               style="fill:#fef3df" /><path
-               d="m 834.496,443.625 c 0,0 18.236,-25.199 0,-41.637 0,0 -13.126,32.012 -40.242,31.502"
-               id="path108"
-               inkscape:connector-curvature="0"
-               style="fill:#fef3df" /><path
-               d="m 786.264,431.965 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 10e-4,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.979,43.855 95.979,97.952 0,54.098 -42.972,97.953 -95.979,97.953 z"
-               id="path110"
-               inkscape:connector-curvature="0"
-               style="fill:#f1f2f2" /></g><g
-             id="g112"><path
-               d="m 781.737,436.751 c 66.396,0 120.221,53.827 120.221,120.223 0,30.718 -11.526,58.74 -30.482,79.991 21.636,-21.74 35.01,-51.708 35.01,-84.803 0,-66.395 -53.825,-120.222 -120.222,-120.222 -35.678,0 -67.721,15.549 -89.739,40.233 21.772,-21.879 51.91,-35.422 85.212,-35.422 z"
-               id="path114"
-               inkscape:connector-curvature="0"
-               style="fill:#ffffff" /></g></g><path
-           d="m 919.566,695.944 c 0,0 7.562,0.712 13.317,-0.502 l 13.013,16.12 c 0,0 -17.639,9.525 -26.33,12.523 l 0,-28.141 z"
-           id="path116"
-           inkscape:connector-curvature="0"
-           style="opacity:0.1;fill:#3d5263" /></g><path
-         d="m 648.292,659.614 0,81.645 c 0,9.72 7.88,17.6 17.6,17.6 l 236.073,0 c 9.72,0 17.6,-7.88 17.6,-17.6 l 0,-24.902 c 10e-4,0 -175.814,35.524 -271.273,-56.743 z"
-         id="path124"
+     transform="translate(50,10)"><g
+       id="grey_5_"><path
+         d="m 55.948,213.25 c 0.07331,-20.26146 -0.716379,-17.26061 -3.655806,-39.26743 2.227824,-22.4392 -7.627923,-38.85857 -7.669233,-58.34044 0,-4.715 -5.805961,-6.78013 -4.760961,-11.13713 -6.292,13.037 -9.833,27.707 -9.833,43.222 0,25.946 9.89,49.533 26.027,67.059 -0.048,-0.511 -0.082,-1.023 -0.108,-1.536 z"
+         id="path6"
+         inkscape:connector-curvature="0"
+         style="fill:#3d5263"
+         sodipodi:nodetypes="ccccscc" /></g><g
+       id="yellow"><path
+         d="m 254.808,180.412 -0.567,0.455 c -10.49,39.88 -40.951,71.658 -80.048,83.996 l 10.952,9.206 53.296,44.799 31.601,26.563 c 0.783,-2.011 1.229,-4.19 1.231,-6.478 0,-0.007 10e-4,-0.013 10e-4,-0.02 l 0,-16.836 0,-10e-4 0,-28.141 0,-126.736 -16.466,13.193 z"
+         id="path9"
+         inkscape:connector-curvature="0"
+         style="fill:#f9e82d" /><path
+         d="m 23.027,185.52 -6.574,-5.225 -16.452,-13.076 0,90.407 0,81.307 c 0,2.295 0.447,4.481 1.233,6.499 l 58.39,-48.683 26.964,-22.481 12.38,-10.321 C 62.73,251.524 34.307,222.274 23.027,185.52 Z"
+         id="path11"
+         inkscape:connector-curvature="0"
+         style="fill:#f9e82d" /><path
+         d="m 238.441,318.868 -53.296,-44.799 -10.952,-9.206 c -11.431,3.607 -23.597,5.558 -36.22,5.558 -13.653,0 -26.772,-2.28 -39.004,-6.474 l -12.38,10.321 -26.965,22.482 -58.39,48.683 c 2.605,6.69 9.094,11.438 16.706,11.438 l 235.394,0 c 7.613,0 14.103,-4.749 16.707,-11.44 l -31.6,-26.563 z"
+         id="path13"
+         inkscape:connector-curvature="0"
+         style="fill:#edd514;fill-opacity:0.89499996" /></g><g
+       id="grey_4_"><path
+         enable-background="new    "
+         d="M 238.441,318.868 C 196.984,322.876 123.368,324.434 59.625,296.75 38.082,287.394 17.666,274.7 0.002,257.627 l 0,81.307 c 0,2.295 0.447,4.481 1.233,6.499 2.605,6.69 9.094,11.438 16.706,11.438 l 235.394,0 c 7.613,0 14.103,-4.749 16.707,-11.44 0.783,-2.011 1.229,-4.19 1.231,-6.478 l 0,-24.584 c 0,0 -12.58,2.541 -32.832,4.499 z"
+         id="path16"
+         inkscape:connector-curvature="0"
+         style="opacity:0.1;fill:#3d5263" /><path
+         enable-background="new    "
+         d="m 86.588,274.268 c 14.979,6.703 31.579,10.435 49.051,10.435 17.648,0 34.408,-3.803 49.505,-10.634 37.082,-16.777 64.125,-51.824 69.664,-93.657 l -0.567,0.455 c -10.49,39.88 -40.951,71.658 -80.048,83.996 -11.431,3.607 -23.597,5.558 -36.22,5.558 -13.653,0 -26.772,-2.28 -39.004,-6.474 C 62.731,251.524 34.308,222.274 23.028,185.52 l -6.574,-5.225 c 5.525,42.054 32.786,77.261 70.134,93.973 z"
+         id="path18"
          inkscape:connector-curvature="0"
          style="opacity:0.1;fill:#3d5263" /></g><g
-       id="g126" /></g></svg>
+       id="white_1_"><path
+         d="m 54.293,63.875 c -1.799,1.745 -3.541,3.548 -5.229,5.402 -0.042,0.046 -0.085,0.092 -0.127,0.139 -0.234,0.258 -0.473,0.51 -0.705,0.77 0.055,-0.055 0.111,-0.108 0.166,-0.163 21.76,-21.782 51.828,-35.259 85.046,-35.259 66.396,0 120.222,53.826 120.222,120.223 0,30.718 -11.526,58.74 -30.482,79.991 21.633,-21.737 35.006,-51.7 35.01,-84.791 0,-0.004 0,-0.009 0,-0.013 0,-21.143 -5.465,-41.007 -15.049,-58.269 -1.449,-2.608 -2.991,-5.157 -4.624,-7.643 -5.377,-8.187 -11.727,-15.676 -18.885,-22.307 -5.903,-5.467 -12.351,-10.354 -19.26,-14.558 -4.278,-2.604 -8.734,-4.944 -13.341,-7.006 -10.627,-4.756 -22.07,-8.016 -34.062,-9.509 -4.915,-0.612 -9.921,-0.931 -15.001,-0.931 -5.747,0 -11.398,0.409 -16.93,1.189 -12.291,1.733 -23.981,5.329 -34.784,10.487 -4.742,2.264 -9.313,4.83 -13.688,7.672 -6.561,4.266 -12.682,9.149 -18.277,14.576 z"
+         id="path21"
+         inkscape:connector-curvature="0"
+         style="fill:#ffffff" /><path
+         d="m 95.828,118.535 c 2.559,0 4.63,-2.071 4.63,-4.629 0,-2.553 -2.071,-4.626 -4.63,-4.626 -2.558,0 -4.634,2.074 -4.634,4.626 10e-4,2.557 2.076,4.629 4.634,4.629 z"
+         id="path23"
+         inkscape:connector-curvature="0"
+         style="fill:#ffffff" /><path
+         d="m 186.85,118.535 c 2.556,0 4.629,-2.071 4.629,-4.629 0,-2.553 -2.074,-4.626 -4.629,-4.626 -2.559,0 -4.631,2.074 -4.631,4.626 0,2.557 2.073,4.629 4.631,4.629 z"
+         id="path25"
+         inkscape:connector-curvature="0"
+         style="fill:#ffffff" /></g><g
+       id="grey_3_"><g
+         id="g28"><path
+           d="m 223.701,234.394 c 18.648,-21.18 29.965,-48.971 29.965,-79.408 0,-66.396 -53.825,-120.223 -120.222,-120.223 -33.218,0 -63.286,13.477 -85.046,35.259 -4.591,5.125 -8.746,10.647 -12.413,16.507 -1.524,2.437 -2.963,4.931 -4.314,7.48 -7.067,13.341 -11.704,28.167 -13.301,43.893 -0.411,4.043 -0.622,8.146 -0.622,12.298 0,3.849 0.188,7.653 0.542,11.409 0.776,8.241 2.38,16.24 4.735,23.912 11.281,36.754 39.703,66.004 75.941,78.427 12.231,4.193 25.351,6.474 39.004,6.474 12.623,0 24.79,-1.95 36.22,-5.558 18.139,-5.725 34.412,-15.64 47.7,-28.603 0.536,-0.522 1.811,-1.867 1.811,-1.867 z m -5.788,-58.356 c -2.132,7.217 -5.052,14.085 -8.668,20.495 -16.571,29.372 -47.64,49.146 -83.233,49.146 -27.584,0 -52.447,-11.88 -69.956,-30.895 C 39.919,197.26 30.03,173.673 30.03,147.726 c 0,-15.515 3.54,-30.185 9.833,-43.222 15.648,-32.42 48.344,-54.73 86.15,-54.73 3.967,0 7.876,0.25 11.717,0.728 47.479,5.898 84.262,47.175 84.262,97.224 -0.002,9.846 -1.431,19.348 -4.079,28.312 z"
+           id="path30"
+           inkscape:connector-curvature="0"
+           style="fill:#f1f2f2" /></g><path
+         d="m 49.064,69.277 c -0.042,0.046 -0.085,0.092 -0.127,0.139 0.042,-0.047 0.085,-0.093 0.127,-0.139 z"
+         id="path32"
+         inkscape:connector-curvature="0"
+         style="fill:#f1f2f2" /></g><g
+       id="darkbrown_1_"><path
+         d="m 257.626,161.89 c -0.488,5.062 -1.29,10.032 -2.387,14.89 -0.31,1.371 -0.643,2.733 -0.999,4.086 l 0.567,-0.455 16.466,-13.193 0,-0.023 -13.647,-5.305 z"
+         id="path35"
+         inkscape:connector-curvature="0"
+         style="fill:#5a3620" /><path
+         d="m 0.001,167.219 16.451,13.076 6.574,5.225 c -2.354,-7.672 -3.959,-15.671 -4.735,-23.912 l -2.85,0.871 L 0,167.196"
+         id="path37"
+         inkscape:connector-curvature="0"
+         style="fill:#5a3620" /><path
+         d="m 87.491,192.337 c -6.21,0 -11.254,5.034 -11.254,11.257 0,6.216 5.043,11.257 11.254,11.257 6.221,0 11.261,-5.041 11.261,-11.257 0,-6.223 -5.041,-11.257 -11.261,-11.257 z"
+         id="path39"
+         inkscape:connector-curvature="0"
+         style="fill:#5a3620" /><path
+         d="m 181.307,192.337 c -6.218,0 -11.259,5.034 -11.259,11.257 0,6.216 5.041,11.257 11.259,11.257 6.22,0 11.257,-5.041 11.257,-11.257 0,-6.223 -5.037,-11.257 -11.257,-11.257 z"
+         id="path41"
+         inkscape:connector-curvature="0"
+         style="fill:#5a3620" /><path
+         d="m 182.997,102.25057 c -6.963,0 -15.44243,7.76632 -15.44243,14.73532 0,6.965 8.12588,17.2072 15.08888,17.2072 6.968,0 15.79898,-9.53609 15.79898,-16.50009 0.001,-6.97 -8.47743,-15.44243 -15.44543,-15.44243 z m 3.853,16.28443 c -2.558,0 -4.631,-2.072 -4.631,-4.629 0,-2.552 2.072,-4.626 4.631,-4.626 2.555,0 4.629,2.073 4.629,4.626 0,2.558 -2.073,4.629 -4.629,4.629 z"
+         id="path43"
+         inkscape:connector-curvature="0"
+         style="fill:#5a3620"
+         sodipodi:nodetypes="ssscssssss" /><path
+         d="m 89.709786,102.60413 c -6.971,0 -14.379767,8.11987 -14.379767,15.08887 0,6.965 8.824981,16.14653 15.793981,16.14653 6.963,0 15.79298,-9.18253 15.79298,-16.14653 0.001,-6.97 -10.243194,-15.08887 -17.207194,-15.08887 z M 95.828,118.535 c -2.559,0 -4.634,-2.072 -4.634,-4.629 0,-2.552 2.076,-4.626 4.634,-4.626 2.559,0 4.63,2.073 4.63,4.626 0,2.558 -2.071,4.629 -4.63,4.629 z"
+         id="path45"
+         inkscape:connector-curvature="0"
+         style="fill:#5a3620"
+         sodipodi:nodetypes="ssscssssss" /></g><g
+       id="cream"><path
+         d="m 336.302,256.425 c 3.59,-9.155 7.701,-11 9.346,-11.346 -40.757,3.757 -36.661,27.769 -34.026,35.96 0.55,1.712 1.037,2.733 1.037,2.733 0,0 2.031,4.787 7.536,8.748 4.149,2.986 10.27,5.503 18.995,5.144 27.063,0.461 35.631,-50.166 35.631,-50.166 -6.654,11.655 -26.404,9.876 -38.519,8.927 z"
+         id="path48"
+         inkscape:connector-curvature="0"
+         style="fill:#fef3df" /><path
+         d="m 48.937,69.415 c 0.042,-0.046 0.085,-0.092 0.127,-0.139 1.688,-1.854 3.43,-3.657 5.229,-5.402 -8.915,-6.977 -24.344,-15.826 -41.744,-11.633 0,0 2.814,20.458 23.437,34.287 3.667,-5.86 7.822,-11.381 12.413,-16.507 -0.055,0.055 -0.111,0.108 -0.166,0.163 0.231,-0.258 0.47,-0.511 0.704,-0.769 z"
+         id="path50"
+         inkscape:connector-curvature="0"
+         style="fill:#fef3df" /><path
+         d="m 258.812,52.242 c -15.831,-3.815 -30.029,3.169 -39.176,9.714 7.158,6.63 13.508,14.12 18.885,22.307 17.763,-13.689 20.291,-32.021 20.291,-32.021 z"
+         id="path52"
+         inkscape:connector-curvature="0"
+         style="fill:#fef3df" /><path
+         d="m 134.269,160.225 c -43.299,0 -78.388,22.964 -78.388,51.289 0,0.582 0.038,1.157 0.067,1.735 0.026,0.514 0.06,1.025 0.108,1.535 17.508,19.015 42.371,30.895 69.956,30.895 35.594,0 66.662,-19.774 83.233,-49.146 -9.796,-21.016 -39.651,-36.308 -74.976,-36.308 z M 87.491,214.85 c -6.211,0 -11.254,-5.041 -11.254,-11.257 0,-6.223 5.044,-11.257 11.254,-11.257 6.22,0 11.261,5.034 11.261,11.257 0,6.216 -5.04,11.257 -11.261,11.257 z m 93.816,0 c -6.218,0 -11.259,-5.041 -11.259,-11.257 0,-6.223 5.041,-11.257 11.259,-11.257 6.22,0 11.257,5.034 11.257,11.257 0,6.216 -5.037,11.257 -11.257,11.257 z"
+         id="path54"
+         inkscape:connector-curvature="0"
+         style="fill:#fef3df" /><path
+         d="M 86.265,0 C 68.102,16.373 86.113,41.427 86.258,41.628 97.061,36.47 108.751,32.874 121.042,31.141 97.629,27.686 86.265,0 86.265,0 Z"
+         id="path56"
+         inkscape:connector-curvature="0"
+         style="fill:#fef3df" /><path
+         d="m 186.204,0 c 0,0 -10.863,26.476 -33.231,30.883 11.992,1.493 23.435,4.752 34.062,9.509 C 190.383,35.136 202.036,14.271 186.204,0 Z"
+         id="path58"
+         inkscape:connector-curvature="0"
+         style="fill:#fef3df" /></g><g
+       id="g60"><path
+         d="m 217.913,176.038 c 2.647,-8.964 6.55187,-25.89162 6.55187,-35.73662 C 224.46487,90.252379 185.208,56.4 137.728,50.502 c -2.157,28.03 3.629,87.043 80.185,125.536 z m -47.53,-58.345 c 0,-6.97 5.651,-12.614 12.614,-12.614 6.968,0 12.617,5.645 12.617,12.614 0,6.964 -5.649,12.611 -12.617,12.611 -6.963,0 -12.614,-5.646 -12.614,-12.611 z"
+         id="path62"
+         inkscape:connector-curvature="0"
+         style="fill:#87654a"
+         sodipodi:nodetypes="csccsssss" /></g><g
+       id="brown"><path
+         d="m 312.658,283.772 c 0,0 -0.487,-1.021 -1.037,-2.733 -3.758,3.317 -13.036,10.236 -27.03,12.416 l 0,-10e-4 c -0.009,0.002 -0.019,0.003 -0.027,0.005 -4.044,0.628 -8.479,0.863 -13.29,0.497 l 0,28.141 c 2.059,-0.801 4.607,-1.834 7.477,-3.083 5.462,-2.377 12.093,-5.542 18.771,-9.395 0.027,-0.016 0.054,-0.031 0.081,-0.047 8.158,-4.713 16.37,-10.452 22.593,-17.052 -5.506,-3.961 -7.538,-8.748 -7.538,-8.748 z"
+         id="path65"
+         inkscape:connector-curvature="0"
+         style="fill:#b58765" /><path
+         d="m 12.549,52.242 c 17.4,-4.193 32.83,4.656 41.744,11.633 C 59.888,58.449 66.009,53.565 72.57,49.301 48.272,18.498 2.169,37.201 2.169,37.201 -1.114,67.502 15.288,84.594 31.672,94.01 33.023,91.461 34.462,88.966 35.986,86.53 15.363,72.699 12.549,52.242 12.549,52.242 Z"
+         id="path67"
+         inkscape:connector-curvature="0"
+         style="fill:#b58765" /><path
+         d="m 200.376,47.398 c 6.909,4.205 13.356,9.091 19.26,14.558 9.146,-6.545 23.345,-13.529 39.176,-9.714 0,0 -2.527,18.332 -20.291,32.021 1.633,2.485 3.175,5.034 4.624,7.643 15.141,-9.784 29.097,-26.539 26.046,-54.704 0,-10e-4 -44.152,-17.909 -68.815,10.196 z"
+         id="path69"
+         inkscape:connector-curvature="0"
+         style="fill:#b58765" /><path
+         d="m 138.854,50.502 c -3.841,-0.478 -8.875,-0.728 -12.842,-0.728 -37.806,0 -70.502,22.31 -86.15,54.73 -1.045,4.357 -1.603,8.897 -1.603,13.612 0,1.454 0.085,2.787 0.121,4.175 0.127,3.935 0.448,7.585 0.855,11.135 4.291755,24.95762 7.959057,42.49186 13.464,66.758 0.056,0.407 0.164,0.804 0.224,1.211 0.617,4.028 1.642,7.992 3.025,11.854 -0.029,-0.578 -0.067,-1.153 -0.067,-1.735 0,-28.325 35.089,-51.289 78.388,-51.289 35.325,0 65.181,15.292 74.977,36.308 3.616,-6.409 6.536,-13.277 8.668,-20.495 C 179.98905,152.54886 163.9995,134.88987 153.25313,111.82124 142.50675,88.752624 137.775,64.517 138.854,50.502 Z m -47.73,79.802 c -6.97,0 -12.612,-5.646 -12.612,-12.611 0,-6.97 5.642,-12.614 12.612,-12.614 6.964,0 12.611,5.645 12.611,12.614 0.001,6.964 -5.648,12.611 -12.611,12.611 z"
+         id="path71"
+         inkscape:connector-curvature="0"
+         style="fill:#b58765"
+         sodipodi:nodetypes="cscscccccssccscssscs" /></g></g></svg>

+ 17 - 104
data/web/inc/footer.inc.php

@@ -12,10 +12,20 @@ logger();
 <script src="/js/formcache.min.js"></script>
 <script src="/js/google.charts.loader.js"></script>
 <script src="/js/numberedtextarea.min.js"></script>
+<script src="/js/sha1.min.js"></script>
 <script src="/js/u2f-api.js"></script>
 <script src="/js/api.js"></script>
+<script src="/js/mailcow.js"></script>
 <script>
-var loading_text = '<?= $lang['footer']['loading']; ?>'
+<?php
+$lang_footer = json_encode($lang['footer']);
+$lang_acl = json_encode($lang['acl']);
+$lang_tfa = json_encode($lang['tfa']);
+echo "var lang_footer = ". $lang_footer . ";\n";
+echo "var lang_acl = ". $lang_acl . ";\n";
+echo "var lang_tfa = ". $lang_tfa . ";\n";
+echo "var docker_timeout = ". $DOCKER_TIMEOUT * 1000 . ";\n";
+?>
 $(window).scroll(function() {
   sessionStorage.scrollTop = $(this).scrollTop();
 });
@@ -28,17 +38,8 @@ $(window).load(function() {
   $(".overlay").hide();
 });
 $(document).ready(function() {
-  window.mailcow_alert_box = function(message, type) {
-    msg = $('<span/>').text(message).text();
-    if (type == 'danger') {
-      auto_hide = 0;
-      $('#' + localStorage.getItem("add_modal")).modal('show');
-      localStorage.removeItem("add_modal");
-    } else {
-      auto_hide = 5000;
-    }
-    $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
-  }
+  // TFA, CSRF, Alerts in footer.inc.php
+  // Other general functions in mailcow.js
   <?php
   $alertbox_log_parser = alertbox_log_parser($_SESSION);
   if (is_array($alertbox_log_parser)) {
@@ -50,7 +51,6 @@ $(document).ready(function() {
   unset($_SESSION['return']);
   }
   ?>
-  $('[data-cached-form="true"]').formcache({key: $(this).data('id')});
   // Confirm TFA modal
   <?php if (isset($_SESSION['pending_tfa_method'])):?>
   $('#ConfirmTFAModal').modal({
@@ -59,7 +59,7 @@ $(document).ready(function() {
   });
   $('#u2f_status_auth').html('<p><span class="glyphicon glyphicon-refresh glyphicon-spin"></span> Initializing, please wait...</p>');
   $('#ConfirmTFAModal').on('shown.bs.modal', function(){
-      $(this).find('#token').focus();
+      $(this).find('input[name=token]').focus();
       // If U2F
       if(document.getElementById("u2f_auth_data") !== null) {
         $.ajax({
@@ -68,7 +68,7 @@ $(document).ready(function() {
           dataType: 'script',
           url: "/api/v1/get/u2f-authentication/<?= (isset($_SESSION['pending_mailcow_cc_username'])) ? rawurlencode($_SESSION['pending_mailcow_cc_username']) : null; ?>",
           complete: function(data){
-            $('#u2f_status_auth').html('<?=$lang['tfa']['waiting_usb_auth'];?>');
+            $('#u2f_status_auth').html(lang_tfa.waiting_usb_auth);
             data;
             setTimeout(function() {
               console.log("Ready to authenticate");
@@ -98,7 +98,6 @@ $(document).ready(function() {
   <?php endif; ?>
 
   // Set TFA modals
-
   $('#selectTFA').change(function () {
     if ($(this).val() == "yubi_otp") {
       $('#YubiOTPModal').modal('show');
@@ -121,7 +120,7 @@ $(document).ready(function() {
           data;
           setTimeout(function() {
             console.log("Ready to register");
-            $('#u2f_status_reg').html('<?=$lang['tfa']['waiting_usb_register'];?>');
+            $('#u2f_status_reg').html(lang_tfa.waiting_usb_register);
             u2f.register(appId, registerRequests, registeredKeys, function(deviceResponse) {
               var form  = document.getElementById('u2f_reg_form');
               var reg   = document.getElementById('u2f_register_data');
@@ -146,94 +145,8 @@ $(document).ready(function() {
     }
   });
 
-  $(function () {
-    $('[data-toggle="tooltip"]').tooltip()
-  });
-
-  // Remember last navigation pill
-  (function () {
-    'use strict';
-    if ($('a[data-toggle="tab"]').length) {
-      $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
-        if ($(this).data('dont-remember') == 1) {
-          return true;
-        }
-        var id = $(this).parents('[role="tablist"]').attr('id');
-        var key = 'lastTag';
-        if (id) {
-          key += ':' + id;
-        }
-        localStorage.setItem(key, $(e.target).attr('href'));
-      });
-      $('[role="tablist"]').each(function (idx, elem) {
-        var id = $(elem).attr('id');
-        var key = 'lastTag';
-        if (id) {
-          key += ':' + id;
-        }
-        var lastTab = localStorage.getItem(key);
-        if (lastTab) {
-          $('[href="' + lastTab + '"]').tab('show');
-        }
-      });
-    }
-  })();
-
-  // Disable submit after submitting form (not API driven buttons)
-  $('form').submit(function() {
-    if ($('form button[type="submit"]').data('submitted') == '1') {
-      return false;
-    } else {
-      $(this).find('button[type="submit"]').first().text('<?= $lang['footer']['loading']; ?>');
-      $('form button[type="submit"]').attr('data-submitted', '1');
-      function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
-      $(document).on("keydown", disableF5);
-    }
-  });
-
-  // IE fix to hide scrollbars when table body is empty
-  $('tbody').filter(function (index) {
-    return $(this).children().length < 1;
-  }).remove();
-
-  // Init Bootstrap Selectpicker
-  $('select').selectpicker();
-
-  // Trigger container restart
-  $('#RestartContainer').on('show.bs.modal', function(e) {
-    var container = $(e.relatedTarget).data('container');
-    $('#containerName').text(container);
-    $('#triggerRestartContainer').click(function(){
-      $(this).prop("disabled",true);
-      $(this).html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> ');
-      $('#statusTriggerRestartContainer').html('<?= $lang['footer']['restarting_container']; ?>');
-      $.ajax({
-        method: 'get',
-        url: '/inc/ajax/container_ctrl.php',
-        timeout: <?= $DOCKER_TIMEOUT * 1000; ?>,
-        data: {
-        'service': container,
-        'action': 'restart'
-        }
-      })
-      .always( function (data, status) {
-        $('#statusTriggerRestartContainer').append(data);
-        var htmlResponse = $.parseHTML(data)
-        if ($(htmlResponse).find('span').hasClass('text-success')) {
-          $('#triggerRestartContainer').html('<span class="glyphicon glyphicon-ok"></span> ');
-          setTimeout(function(){
-            $('#RestartContainer').modal('toggle'); 
-            window.location = window.location.href.split("#")[0];
-          }, 1200);
-        } else {
-          $('#triggerRestartContainer').html('<span class="glyphicon glyphicon-remove"></span> ');
-        }
-      })
-    });
-  })
-
   // CSRF
-  $('<input type="hidden" value="<?= $_SESSION['CSRF']['TOKEN']; ?>">').attr('id', 'csrf_token').attr('name', 'csrf_token').appendTo('form');
+  $('<input type="hidden" value="<?= $_SESSION['CSRF']['TOKEN']; ?>">').attr('name', 'csrf_token').appendTo('form');
   if (sessionStorage.scrollTop != "undefined") {
     $(window).scrollTop(sessionStorage.scrollTop);
   }

+ 216 - 0
data/web/inc/functions.acl.inc.php

@@ -0,0 +1,216 @@
+<?php
+function acl($_action, $_scope = null, $_data = null) {
+  global $pdo;
+  global $lang;
+  $_data_log = $_data;
+  switch ($_action) {
+    case 'edit':
+      switch ($_scope) {
+        case 'user':
+          if (!is_array($_data['username'])) {
+            $usernames = array();
+            $usernames[] = $_data['username'];
+          }
+          else {
+            $usernames = $_data['username'];
+          }
+          foreach ($usernames as $username) {
+            // Cast to array for single selections
+            $acls = (array)$_data['user_acl'];
+            // Create associative array from index array
+            // All set items are given 1 as value
+            foreach ($acls as $acl_key => $acl_val) {
+              $acl_post[$acl_val] = 1;
+            }
+            // Users cannot change their own ACL
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
+              || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+                'msg' => 'access_denied'
+              );
+              continue;
+            }
+            // Read all available acl options by calling acl(get)
+            // Set all available acl options we cannot find in the post data to 0, else 1
+            $is_now = acl('get', 'user', $username);
+            if (!empty($is_now)) {
+              foreach ($is_now as $acl_now_name => $acl_now_val) {
+                $set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
+              }
+            }
+            else {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+                'msg' => 'Cannot determine current ACL'
+              );
+              continue;
+            }
+            foreach ($set_acls as $set_acl_key => $set_acl_val) {
+              $stmt = $pdo->prepare("UPDATE `user_acl` SET " . $set_acl_key . " = " . intval($set_acl_val) . "
+                WHERE `username` = :username");
+              $stmt->execute(array(
+                ':username' => $username,
+              ));
+            }
+            $_SESSION['return'][] = array(
+              'type' => 'success',
+              'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+              'msg' => array('acl_saved', $username)
+            );
+          }
+        break;
+        case 'domainadmin':
+          if ($_SESSION['mailcow_cc_role'] != 'admin') {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          if (!is_array($_data['username'])) {
+            $usernames = array();
+            $usernames[] = $_data['username'];
+          }
+          else {
+            $usernames = $_data['username'];
+          }
+          foreach ($usernames as $username) {
+            // Cast to array for single selections
+            $acls = (array)$_data['da_acl'];
+            // Create associative array from index array
+            // All set items are given 1 as value
+            foreach ($acls as $acl_key => $acl_val) {
+              $acl_post[$acl_val] = 1;
+            }
+            // Users cannot change their own ACL
+            if ($_SESSION['mailcow_cc_role'] != 'admin') {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+                'msg' => 'access_denied'
+              );
+              continue;
+            }
+            // Read all available acl options by calling acl(get)
+            // Set all available acl options we cannot find in the post data to 0, else 1
+            $is_now = acl('get', 'domainadmin', $username);
+            if (!empty($is_now)) {
+              foreach ($is_now as $acl_now_name => $acl_now_val) {
+                $set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
+              }
+            }
+            else {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+                'msg' => 'Cannot determine current ACL'
+              );
+              continue;
+            }
+            foreach ($set_acls as $set_acl_key => $set_acl_val) {
+              $stmt = $pdo->prepare("UPDATE `da_acl` SET " . $set_acl_key . " = " . intval($set_acl_val) . "
+                WHERE `username` = :username");
+              $stmt->execute(array(
+                ':username' => $username,
+              ));
+            }
+            $_SESSION['return'][] = array(
+              'type' => 'success',
+              'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+              'msg' => array('acl_saved', $username)
+            );
+          }
+        break;
+      }
+    break;
+    case 'get':
+      switch ($_scope) {
+        case 'user':
+          if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+            return false;
+          }
+          $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");
+          $stmt->execute(array(':username' => $_data));
+          $data = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (!empty($data)) {
+            unset($data['username']);
+            return $data;
+          }
+          else {
+            return false;
+          }
+        break;
+        case 'domainadmin':
+          if ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin') {
+            return false;
+          }
+          if ($_SESSION['mailcow_cc_role'] == 'domainadmin' && $_SESSION['mailcow_cc_username'] != $_data) {
+            return false;
+          }
+          $stmt = $pdo->prepare("SELECT * FROM `da_acl` WHERE `username` = :username");
+          $stmt->execute(array(':username' => $_data));
+          $data = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (!empty($data)) {
+            unset($data['username']);
+            return $data;
+          }
+          else {
+            return false;
+          }
+        break;
+      }
+    break;
+    case 'to_session':
+      if (!isset($_SESSION['mailcow_cc_role'])) {
+        return false;
+      }
+      unset($_SESSION['acl']);
+      $username = strtolower(trim($_SESSION['mailcow_cc_username']));
+      // Admins get access to all modules
+      if ($_SESSION['mailcow_cc_role'] == 'admin' ||
+        (isset($_SESSION["dual-login"]["role"]) && $_SESSION["dual-login"]["role"] == 'admin')) {
+        $stmt = $pdo->query("SHOW COLUMNS FROM `user_acl` WHERE `Field` != 'username';");
+        $acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
+        while ($row = array_shift($acl_all)) {
+          $acl['acl'][$row['Field']] = 1;
+        }
+        $stmt = $pdo->query("SHOW COLUMNS FROM `da_acl` WHERE `Field` != 'username';");
+        $acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
+        while ($row = array_shift($acl_all)) {
+          $acl['acl'][$row['Field']] = 1;
+        }
+      }
+      elseif ($_SESSION['mailcow_cc_role'] == 'domainadmin' ||
+        (isset($_SESSION["dual-login"]["role"]) && $_SESSION["dual-login"]["role"] == 'domainadmin')) {
+        // Read all exting user_acl modules and set to 1
+        $stmt = $pdo->query("SHOW COLUMNS FROM `user_acl` WHERE `Field` != 'username';");
+        $acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
+        while ($row = array_shift($acl_all)) {
+          $acl['acl'][$row['Field']] = 1;
+        }
+        // Read da_acl rules for current user, OVERWRITE overlapping modules
+        // This prevents access to a users sync jobs, when a domain admins was not given access to sync jobs
+        $stmt = $pdo->prepare("SELECT * FROM `da_acl` WHERE `username` = :username");
+        $stmt->execute(array(':username' => (isset($_SESSION["dual-login"]["username"])) ? $_SESSION["dual-login"]["username"] : $username));
+        $acl_user = $stmt->fetch(PDO::FETCH_ASSOC);
+        foreach ($acl_user as $acl_user_key => $acl_user_val) {
+          $acl['acl'][$acl_user_key] = $acl_user_val;
+        }
+        unset($acl['acl']['username']);
+      }
+      elseif ($_SESSION['mailcow_cc_role'] == 'user') {
+        $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");
+        $stmt->execute(array(':username' => $username));
+        $acl['acl'] = $stmt->fetch(PDO::FETCH_ASSOC);
+        unset($acl['acl']['username']);
+      }
+      if (!empty($acl)) {
+        $_SESSION = array_merge($_SESSION, $acl);
+      }
+    break;
+  }
+}

+ 76 - 130
data/web/inc/functions.address_rewriting.inc.php

@@ -87,25 +87,15 @@ function bcc($_action, $_data = null, $attr = null) {
         );
         return false;
       }
-      try {
-        $stmt = $pdo->prepare("INSERT INTO `bcc_maps` (`local_dest`, `bcc_dest`, `domain`, `active`, `type`) VALUES
-          (:local_dest, :bcc_dest, :domain, :active, :type)");
-        $stmt->execute(array(
-          ':local_dest' => $local_dest_sane,
-          ':bcc_dest' => $bcc_dest,
-          ':domain' => $domain,
-          ':active' => $active,
-          ':type' => $type
-        ));
-      }
-      catch (PDOException $e) {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => array('mysql_error', $e)
-        );
-        return false;
-      }
+      $stmt = $pdo->prepare("INSERT INTO `bcc_maps` (`local_dest`, `bcc_dest`, `domain`, `active`, `type`) VALUES
+        (:local_dest, :bcc_dest, :domain, :active, :type)");
+      $stmt->execute(array(
+        ':local_dest' => $local_dest_sane,
+        ':bcc_dest' => $bcc_dest,
+        ':domain' => $domain,
+        ':active' => $active,
+        ':type' => $type
+      ));
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -155,37 +145,27 @@ function bcc($_action, $_data = null, $attr = null) {
           );
           continue;
         }
-        try {
-          $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
-            WHERE `local_dest` = :local_dest AND `type` = :type");
-          $stmt->execute(array(':local_dest' => $local_dest, ':type' => $type));
-          $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
+        $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
+          WHERE `local_dest` = :local_dest AND `type` = :type");
+        $stmt->execute(array(':local_dest' => $local_dest, ':type' => $type));
+        $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
 
-          if (isset($id_now) && $id_now != $id) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-              'msg' => array('bcc_exists', htmlspecialchars($local_dest), $type)
-            );
-            continue;
-          }
-
-          $stmt = $pdo->prepare("UPDATE `bcc_maps` SET `bcc_dest` = :bcc_dest, `active` = :active, `type` = :type WHERE `id`= :id");
-          $stmt->execute(array(
-            ':bcc_dest' => $bcc_dest,
-            ':active' => $active,
-            ':type' => $type,
-            ':id' => $id
-          ));
-        }
-        catch (PDOException $e) {
+        if (isset($id_now) && $id_now != $id) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
+            'msg' => array('bcc_exists', htmlspecialchars($local_dest), $type)
           );
           continue;
         }
+
+        $stmt = $pdo->prepare("UPDATE `bcc_maps` SET `bcc_dest` = :bcc_dest, `active` = :active, `type` = :type WHERE `id`= :id");
+        $stmt->execute(array(
+          ':bcc_dest' => $bcc_dest,
+          ':active' => $active,
+          ':type' => $type,
+          ':id' => $id
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -233,34 +213,33 @@ function bcc($_action, $_data = null, $attr = null) {
       return $bccdata;
     break;
     case 'delete':
+      if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => 'access_denied'
+        );
+        return false;
+      }
       $ids = (array)$_data['id'];
       foreach ($ids as $id) {
         if (!is_numeric($id)) {
           return false;
         }
-        try {
-          $stmt = $pdo->prepare("SELECT `domain` FROM `bcc_maps` WHERE id = :id");
-          $stmt->execute(array(':id' => $id));
-          $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain'];
-          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-              'msg' => 'access_denied'
-            );
-            continue;
-          }
-          $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `id`= :id");
-          $stmt->execute(array(':id' => $id));
-        }
-        catch (PDOException $e) {
+        $stmt = $pdo->prepare("SELECT `domain` FROM `bcc_maps` WHERE id = :id");
+        $stmt->execute(array(':id' => $id));
+        $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain'];
+        if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
+            'msg' => 'access_denied'
           );
           continue;
         }
+        $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `id`= :id");
+        $stmt->execute(array(':id' => $id));
+
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -309,33 +288,22 @@ function recipient_map($_action, $_data = null, $attr = null) {
       }
       $rmaps = recipient_map('get');
       foreach ($rmaps as $rmap) {
-        $old_dests_existing[] = recipient_map('details', $rmap)['recipient_map_old'];
-      }
-      if (in_array($old_dest_sane, $old_dests_existing)) {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest))
-        );
-        return false;
-      }
-      try {
-        $stmt = $pdo->prepare("INSERT INTO `recipient_maps` (`old_dest`, `new_dest`, `active`) VALUES
-          (:old_dest, :new_dest, :active)");
-        $stmt->execute(array(
-          ':old_dest' => $old_dest_sane,
-          ':new_dest' => $new_dest,
-          ':active' => $active
-        ));
-      }
-      catch (PDOException $e) {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => array('mysql_error', $e)
-        );
-        return false;
+        if (recipient_map('details', $rmap)['recipient_map_old'] == $old_dest_sane) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+            'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest_sane))
+          );
+          return false;
+        }
       }
+      $stmt = $pdo->prepare("INSERT INTO `recipient_maps` (`old_dest`, `new_dest`, `active`) VALUES
+        (:old_dest, :new_dest, :active)");
+      $stmt->execute(array(
+        ':old_dest' => $old_dest_sane,
+        ':new_dest' => $new_dest,
+        ':active' => $active
+      ));
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -376,7 +344,6 @@ function recipient_map($_action, $_data = null, $attr = null) {
           );
           continue;
         }
-        $active = intval($_data['active']);
         if (!filter_var($new_dest, FILTER_VALIDATE_EMAIL)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
@@ -387,38 +354,27 @@ function recipient_map($_action, $_data = null, $attr = null) {
         }
         $rmaps = recipient_map('get');
         foreach ($rmaps as $rmap) {
-          $old_dests_existing[] = recipient_map('details', $rmap)['recipient_map_old'];
-        }
-        if (in_array($old_dest_sane, $old_dests_existing) &&
-          recipient_map('details', $id)['recipient_map_old'] != $old_dest_sane) {
+          if ($rmap == $id) { continue; }
+          if (recipient_map('details', $rmap)['recipient_map_old'] == $old_dest_sane) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_data, $_attr),
               'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest_sane))
             );
-            continue;
-        }
-        try {
-          $stmt = $pdo->prepare("UPDATE `recipient_maps` SET
-            `old_dest` = :old_dest,
-            `new_dest` = :new_dest,
-            `active` = :active
-              WHERE `id`= :id");
-          $stmt->execute(array(
-            ':old_dest' => $old_dest_sane,
-            ':new_dest' => $new_dest,
-            ':active' => $active,
-            ':id' => $id
-          ));
-        }
-        catch (PDOException $e) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
-          );
-          return false;
+            return false;
+          }
         }
+        $stmt = $pdo->prepare("UPDATE `recipient_maps` SET
+          `old_dest` = :old_dest,
+          `new_dest` = :new_dest,
+          `active` = :active
+            WHERE `id`= :id");
+        $stmt->execute(array(
+          ':old_dest' => $old_dest_sane,
+          ':new_dest' => $new_dest,
+          ':active' => $active,
+          ':id' => $id
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
@@ -463,24 +419,14 @@ function recipient_map($_action, $_data = null, $attr = null) {
         if (!is_numeric($id)) {
           return false;
         }
-        try {
-          $stmt = $pdo->prepare("DELETE FROM `recipient_maps` WHERE `id`= :id");
-          $stmt->execute(array(':id' => $id));
-        }
-        catch (PDOException $e) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-            'msg' => array('mysql_error', $e)
-          );
-          return false;
-        }
+        $stmt = $pdo->prepare("DELETE FROM `recipient_maps` WHERE `id`= :id");
+        $stmt->execute(array(':id' => $id));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => array('recipient_map_entry_deleted', htmlspecialchars($id))
+        );
       }
-      $_SESSION['return'][] = array(
-        'type' => 'success',
-        'msg' => array('recipient_map_entry_deleted', htmlspecialchars($old_dest))
-      );
-      return true;
     break;
   }
 }

+ 18 - 15
data/web/inc/functions.docker.inc.php

@@ -2,10 +2,13 @@
 function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $extra_headers = null) {
   global $DOCKER_TIMEOUT;
   $curl = curl_init();
-  curl_setopt($curl, CURLOPT_HTTPHEADER,array( 'Content-Type: application/json' ));
+  curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' ));
+  // We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway
+  curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
+  curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
   switch($action) {
     case 'get_id':
-      curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/json');
+      curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
       curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
       curl_setopt($curl, CURLOPT_POST, 0);
       curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -38,7 +41,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
       }
       return false;
     case 'containers':
-      curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/json');
+      curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
       curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
       curl_setopt($curl, CURLOPT_POST, 0);
       curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -74,7 +77,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
     break;
     case 'info':
       if (empty($service_name)) {
-        curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/json');
+        curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
         curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
         curl_setopt($curl, CURLOPT_POST, 0);
         curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -82,7 +85,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
       else {
         $container_id = docker('get_id', $service_name);
         if (ctype_xdigit($container_id)) {
-          curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/json');
+          curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/' . $container_id . '/json');
         }
         else {
           // logger(array('return' => array(
@@ -144,7 +147,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
       if (!empty($attr1)) {
         $container_id = docker('get_id', $service_name);
         if (ctype_xdigit($container_id) && ctype_alnum($attr1)) {
-          curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $attr1);
+          curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/' . $container_id . '/' . $attr1);
           curl_setopt($curl, CURLOPT_POST, 1);
           curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
           if (!empty($attr2)) {
@@ -158,19 +161,19 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
           if ($response === false) {
             $err = curl_error($curl);
             curl_close($curl);
-            logger(array('return' => array(array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $action, $service_name, $attr1, $attr2, $extra_headers),
-              'msg' => $err,
-            ))));
+            // logger(array('return' => array(array(
+              // 'type' => 'danger',
+              // 'log' => array(__FUNCTION__, $action, $service_name, $attr1, $attr2, $extra_headers),
+              // 'msg' => $err,
+            // ))));
             return $err;
           }
           else {
             curl_close($curl);
-            logger(array('return' => array(array(
-              'type' => 'success',
-              'log' => array(__FUNCTION__, $action, $service_name, $attr1, $attr2, $extra_headers),
-            ))));
+            // logger(array('return' => array(array(
+              // 'type' => 'success',
+              // 'log' => array(__FUNCTION__, $action, $service_name, $attr1, $attr2, $extra_headers),
+            // ))));
             if (empty($response)) {
               return true;
             }

+ 67 - 147
data/web/inc/functions.domain_admin.inc.php

@@ -90,43 +90,22 @@ function domain_admin($_action, $_data = null) {
             );
             return false;
           }
-          try {
-            $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
-                VALUES (:username, :domain, :created, :active)");
-            $stmt->execute(array(
-              ':username' => $username,
-              ':domain' => $domain,
-              ':created' => date('Y-m-d H:i:s'),
-              ':active' => $active
-            ));
-          }
-          catch (PDOException $e) {
-            domain_admin('delete', $username);
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => array('mysql_error', $e)
-            );
-            return false;
-          }
-        }
-        try {
-          $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
-            VALUES (:username, :password_hashed, '0', :active)");
+          $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+              VALUES (:username, :domain, :created, :active)");
           $stmt->execute(array(
             ':username' => $username,
-            ':password_hashed' => $password_hashed,
+            ':domain' => $domain,
+            ':created' => date('Y-m-d H:i:s'),
             ':active' => $active
           ));
         }
-        catch (PDOException $e) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data_log),
-            'msg' => array('mysql_error', $e)
-          );
-          return false;
-        }
+        $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
+          VALUES (:username, :password_hashed, '0', :active)");
+        $stmt->execute(array(
+          ':username' => $username,
+          ':password_hashed' => $password_hashed,
+          ':active' => $active
+        ));
       }
       else {
         $_SESSION['return'][] = array(
@@ -136,6 +115,10 @@ function domain_admin($_action, $_data = null) {
         );
         return false;
       }
+      $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
+      $stmt->execute(array(
+        ':username' => $username
+      ));
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -179,15 +162,14 @@ function domain_admin($_action, $_data = null) {
           $password     = $_data['password'];
           $password2    = $_data['password2'];
           if (!empty($domains)) {
-            foreach ($domains as $i => &$domain) {
+            foreach ($domains as $domain) {
               if (!is_valid_domain_name($domain)) {
                 $_SESSION['return'][] = array(
                   'type' => 'danger',
                   'log' => array(__FUNCTION__, $_action, $_data_log),
                   'msg' => array('domain_invalid', htmlspecialchars($domain))
                 );
-                unset($domains[$i]);
-                continue;
+                continue 2;
               }
             }
           }
@@ -209,44 +191,22 @@ function domain_admin($_action, $_data = null) {
               continue;
             }
           }
-          try {
-            $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
-            $stmt->execute(array(
-              ':username' => $username,
-            ));
-          }
-          catch (PDOException $e) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => array('mysql_error', $e)
-            );
-            continue;
-          }
-
+          $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+          $stmt->execute(array(
+            ':username' => $username,
+          ));
           if (!empty($domains)) {
             foreach ($domains as $domain) {
-              try {
-                $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
-                  VALUES (:username_new, :domain, :created, :active)");
-                $stmt->execute(array(
-                  ':username_new' => $username_new,
-                  ':domain' => $domain,
-                  ':created' => date('Y-m-d H:i:s'),
-                  ':active' => $active
-                ));
-              }
-              catch (PDOException $e) {
-                $_SESSION['return'][] = array(
-                  'type' => 'danger',
-                  'log' => array(__FUNCTION__, $_action, $_data_log),
-                  'msg' => array('mysql_error', $e)
-                );
-                continue;
-              }
+              $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+                VALUES (:username_new, :domain, :created, :active)");
+              $stmt->execute(array(
+                ':username_new' => $username_new,
+                ':domain' => $domain,
+                ':created' => date('Y-m-d H:i:s'),
+                ':active' => $active
+              ));
             }
           }
-
           if (!empty($password) && !empty($password2)) {
             if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
               $_SESSION['return'][] = array(
@@ -265,56 +225,36 @@ function domain_admin($_action, $_data = null) {
               continue;
             }
             $password_hashed = hash_password($password);
-            try {
-              $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
-              $stmt->execute(array(
-                ':password_hashed' => $password_hashed,
-                ':username_new' => $username_new,
-                ':username' => $username,
-                ':active' => $active
-              ));
-              if (isset($_data['disable_tfa'])) {
-                $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
-                $stmt->execute(array(':username' => $username));
-              }
-              else {
-                $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
-                $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
-              }
+            $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
+            $stmt->execute(array(
+              ':password_hashed' => $password_hashed,
+              ':username_new' => $username_new,
+              ':username' => $username,
+              ':active' => $active
+            ));
+            if (isset($_data['disable_tfa'])) {
+              $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
+              $stmt->execute(array(':username' => $username));
             }
-            catch (PDOException $e) {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_data_log),
-                'msg' => array('mysql_error', $e)
-              );
-              continue;
+            else {
+              $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
+              $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
             }
           }
           else {
-            try {
-              $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
-              $stmt->execute(array(
-                ':username_new' => $username_new,
-                ':username' => $username,
-                ':active' => $active
-              ));
-              if (isset($_data['disable_tfa'])) {
-                $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
-                $stmt->execute(array(':username' => $username));
-              }
-              else {
-                $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
-                $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
-              }
+            $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
+            $stmt->execute(array(
+              ':username_new' => $username_new,
+              ':username' => $username,
+              ':active' => $active
+            ));
+            if (isset($_data['disable_tfa'])) {
+              $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
+              $stmt->execute(array(':username' => $username));
             }
-            catch (PDOException $e) {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_data_log),
-                'msg' => array('mysql_error', $e)
-              );
-              continue;
+            else {
+              $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
+              $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
             }
           }
           $_SESSION['return'][] = array(
@@ -365,21 +305,11 @@ function domain_admin($_action, $_data = null) {
             return false;
           }
           $password_hashed = hash_password($password_new);
-          try {
-            $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
-            $stmt->execute(array(
-              ':password_hashed' => $password_hashed,
-              ':username' => $username
-            ));
-          }
-          catch (PDOException $e) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => array('mysql_error', $e)
-            );
-            return false;
-          }
+          $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
+          $stmt->execute(array(
+            ':password_hashed' => $password_hashed,
+            ':username' => $username
+          ));
         }
         $_SESSION['return'][] = array(
           'type' => 'success',
@@ -407,24 +337,14 @@ function domain_admin($_action, $_data = null) {
           );
           continue;
         }
-        try {
-          $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
-          $stmt->execute(array(
-            ':username' => $username,
-          ));
-          $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
-          $stmt->execute(array(
-            ':username' => $username,
-          ));
-        }
-        catch (PDOException $e) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data_log),
-            'msg' => array('mysql_error', $e)
-          );
-          continue;
-        }
+        $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+        $stmt->execute(array(
+          ':username' => $username,
+        ));
+        $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
+        $stmt->execute(array(
+          ':username' => $username,
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data_log),

+ 37 - 61
data/web/inc/functions.inc.php

@@ -18,6 +18,17 @@ function last_login($user) {
     return false;
   }
 }
+function flush_memcached() {
+  try {
+    $m = new Memcached();
+    $m->addServer('memcached', 11211);
+    $m->flush();
+  }
+  catch ( Exception $e ) {
+    // Dunno
+  }
+}
+
 function logger($_data = false) {
   /*
   logger() will be called as last function
@@ -64,17 +75,23 @@ function logger($_data = false) {
         $user = 'unauthenticated';
         $role = 'unauthenticated';
       }
-      $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
-        (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
-      $stmt->execute(array(
-        ':type' => $type,
-        ':task' => $task,
-        ':call' => $call,
-        ':msg' => $msg,
-        ':user' => $user,
-        ':role' => $role,
-        ':remote' => get_remote_ip()
-      ));
+      // We cannot log when logs is missing...
+      try {
+        $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
+          (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
+        $stmt->execute(array(
+          ':type' => $type,
+          ':task' => $task,
+          ':call' => $call,
+          ':msg' => $msg,
+          ':user' => $user,
+          ':role' => $role,
+          ':remote' => get_remote_ip()
+        ));
+      }
+      catch (Exception $e) {
+        // Do nothing
+      }
     }
   }
   else {
@@ -260,6 +277,12 @@ function verify_hash($hash, $password) {
       return true;
     }
   }
+  elseif (preg_match('/^{MD5-CRYPT}/i', $hash)) {
+    $hash = preg_replace('/^{MD5-CRYPT}/i', '', $hash);
+    if (password_verify($password, $hash)) {
+      return true;
+    }
+  }
   return false;
 }
 function check_login($user, $pass) {
@@ -373,54 +396,6 @@ function check_login($user, $pass) {
 	sleep($_SESSION['ldelay']);
   return false;
 }
-function set_acl() {
-	global $pdo;
-	if (!isset($_SESSION['mailcow_cc_username'])) {
-		return false;
-	}
-	if ($_SESSION['mailcow_cc_role'] == 'admin' || $_SESSION['mailcow_cc_role'] == 'domainadmin') {
-    $stmt = $pdo->query("SHOW COLUMNS FROM `user_acl` WHERE `Field` != 'username';");
-    $acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
-    while ($row = array_shift($acl_all)) {
-      $acl['acl'][$row['Field']] = 1;
-    }
-	}
-  else {
-    $username = strtolower(trim($_SESSION['mailcow_cc_username']));
-    $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");
-    $stmt->execute(array(':username' => $username));
-    $acl['acl'] = $stmt->fetch(PDO::FETCH_ASSOC);
-    unset($acl['acl']['username']);
-  }
-  if (!empty($acl)) {
-    $_SESSION = array_merge($_SESSION, $acl);
-  }
-  else {
-    $_SESSION['return'][] =  array(
-      'type' => 'info',
-      'log' => array(__FUNCTION__),
-      'msg' => 'set_acl_failed'
-    );
-    return false;
-  }
-}
-function get_acl($username) {
-	global $pdo;
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		return false;
-	}
-  $username = strtolower(trim($username));
-  $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");
-  $stmt->execute(array(':username' => $username));
-  $acl = $stmt->fetch(PDO::FETCH_ASSOC);
-  unset($acl['username']);
-  if (!empty($acl)) {
-    return $acl;
-  }
-  else {
-    return false;
-  }
-}
 function formatBytes($size, $precision = 2) {
 	if(!is_numeric($size)) {
 		return "0";
@@ -546,6 +521,7 @@ function update_sogo_static_view() {
     $stmt = $pdo->query("REPLACE INTO _sogo_static_view SELECT * from sogo_view");
     $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
   }
+  flush_memcached();
 }
 function edit_user_account($_data) {
 	global $lang;
@@ -1266,7 +1242,7 @@ function get_admin_details() {
     return false;
   }
   $stmt = $pdo->query("SELECT `admin`.`username`, `api`.`active` AS `api_active`, `api`.`api_key`, `api`.`allow_from` FROM `admin`
-    INNER JOIN `api` ON `admin`.`username` = `api`.`username`
+    LEFT OUTER JOIN `api` ON `admin`.`username` = `api`.`username`
     WHERE `admin`.`superadmin`='1'
       AND `admin`.`active`='1'");
   $data = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -1433,7 +1409,7 @@ function get_logs($container, $lines = false) {
   }
   if ($container == "rspamd-history") {
     $curl = curl_init();
-    curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/rspamd-sock/rspamd.sock');
+    curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
     if (!is_numeric($lines)) {
       list ($from, $to) = explode('-', $lines);
       curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));

+ 63 - 35
data/web/inc/functions.mailbox.inc.php

@@ -3,6 +3,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
   global $pdo;
   global $redis;
   global $lang;
+  global $MAILBOX_DEFAULT_ATTRIBUTES;
   $_data_log = $_data;
   !isset($_data_log['password']) ?: $_data_log['password'] = '*';
   !isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
@@ -139,7 +140,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_name` = 'inactive' WHERE `username` = :username AND `filter_type` = :filter_type");
             $stmt->execute(array(
               ':username' => $username,
-              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
               ':filter_type' => $filter_type
             ));
           }
@@ -637,7 +637,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          foreach ($alias_domains as $i => &$alias_domain) {
+          foreach ($alias_domains as $i => $alias_domain) {
             $alias_domain = idn_to_ascii(strtolower(trim($alias_domain)));
             if (!is_valid_domain_name($alias_domain)) {
               $_SESSION['return'][] = array(
@@ -680,30 +680,30 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-          }
-          $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`)
-            VALUES (:alias_domain, :target_domain, :active)");
-          $stmt->execute(array(
-            ':alias_domain' => $alias_domain,
-            ':target_domain' => $target_domain,
-            ':active' => $active
-          ));
-          try {
-            $redis->hSet('DOMAIN_MAP', $alias_domain, 1);
-          }
-          catch (RedisException $e) {
+            $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`)
+              VALUES (:alias_domain, :target_domain, :active)");
+            $stmt->execute(array(
+              ':alias_domain' => $alias_domain,
+              ':target_domain' => $target_domain,
+              ':active' => $active
+            ));
+            try {
+              $redis->hSet('DOMAIN_MAP', $alias_domain, 1);
+            }
+            catch (RedisException $e) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => array('redis_error', $e)
+              );
+              return false;
+            }
             $_SESSION['return'][] = array(
-              'type' => 'danger',
+              'type' => 'success',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-              'msg' => array('redis_error', $e)
+              'msg' => array('aliasd_added', htmlspecialchars($alias_domain))
             );
-            return false;
           }
-          $_SESSION['return'][] = array(
-            'type' => 'success',
-            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-            'msg' => array('aliasd_added', htmlspecialchars($alias_domain))
-          );
         break;
         case 'mailbox':
           $local_part   = strtolower(trim($_data['local_part']));
@@ -735,6 +735,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $active = intval($_data['active']);
           $quota_b		= ($quota_m * 1048576);
           $maildir		= $domain . "/" . $local_part . "/";
+          $mailbox_attrs = json_encode(
+            array(
+              'force_pw_update' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'])),
+              'tls_enforce_in' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'])),
+              'tls_enforce_out' => strval(intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'])))
+            );
           if (!is_valid_domain_name($domain)) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
@@ -868,7 +874,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `attributes`, `active`) 
-            VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, '{\"force_pw_update\": \"0\", \"tls_enforce_in\": \"0\", \"tls_enforce_out\": \"0\"}', :active)");
+            VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :mailbox_attrs, :active)");
           $stmt->execute(array(
             ':username' => $username,
             ':password_hashed' => $password_hashed,
@@ -877,6 +883,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':quota_b' => $quota_b,
             ':local_part' => $local_part,
             ':domain' => $domain,
+            ':mailbox_attrs' => $mailbox_attrs,
             ':active' => $active
           ));
           $stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
@@ -1453,14 +1460,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
         break;
         case 'filter':
-          $sieve = new Sieve\SieveParser();
-          if (!is_array($_data['id'])) {
-            $ids = array();
-            $ids[] = $_data['id'];
-          }
-          else {
-            $ids = $_data['id'];
-          }
           if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
@@ -1469,6 +1468,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
+          $sieve = new Sieve\SieveParser();
+          if (!is_array($_data['id'])) {
+            $ids = array();
+            $ids[] = $_data['id'];
+          }
+          else {
+            $ids = $_data['id'];
+          }
           foreach ($ids as $id) {
             $is_now = mailbox('get', 'filter_details', $id);
             if (!empty($is_now)) {
@@ -2777,6 +2784,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`active` AS `active_int`,
               CASE `mailbox`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
               `mailbox`.`domain`,
+              `mailbox`.`maildir`,
               `mailbox`.`quota`,
               `quota2`.`bytes`,
               `attributes`,
@@ -2807,6 +2815,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['active'] = $row['active'];
           $mailboxdata['active_int'] = $row['active_int'];
           $mailboxdata['domain'] = $row['domain'];
+          $mailboxdata['maildir'] = $row['maildir'];
           $mailboxdata['quota'] = $row['quota'];
           $mailboxdata['attributes'] = json_decode($row['attributes'], true);
           $mailboxdata['quota_used'] = intval($row['bytes']);
@@ -3055,6 +3064,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
+            $exec_fields = array('cmd' => 'maildir_cleanup', 'maildir' => $domain);
+            $maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
+            if ($maildir_gc['type'] != 'success') {
+              $_SESSION['return'][] = array(
+                'type' => 'warning',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
+              );
+            }
             $stmt = $pdo->prepare("DELETE FROM `domain` WHERE `domain` = :domain");
             $stmt->execute(array(
               ':domain' => $domain,
@@ -3079,17 +3097,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt->execute(array(
               ':domain' => '%@'.$domain,
             ));
-            $stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :domain");
+            $stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` LIKE :domain");
             $stmt->execute(array(
               ':domain' => '%@'.$domain,
             ));
-            $stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `address` = :domain");
+            $stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `address` LIKE :domain");
             $stmt->execute(array(
               ':domain' => '%@'.$domain,
             ));
             $stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :domain");
             $stmt->execute(array(
-              ':domain' => '%@'.$domain,
+              ':domain' => $domain,
             ));
             $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :domain");
             $stmt->execute(array(
@@ -3228,6 +3246,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
+            $maildir = mailbox('get', 'mailbox_details', $username)['maildir'];
+            $exec_fields = array('cmd' => 'maildir_cleanup', 'maildir' => $maildir);
+            $maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
+            if ($maildir_gc['type'] != 'success') {
+              $_SESSION['return'][] = array(
+                'type' => 'warning',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
+              );
+            }
             $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");
             $stmt->execute(array(
               ':username' => $username
@@ -3394,4 +3422,4 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
   if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox'))) {
     update_sogo_static_view();
   }
-}
+}

+ 16 - 16
data/web/inc/functions.policy.inc.php

@@ -6,6 +6,14 @@ function policy($_action, $_scope, $_data = null) {
 	$_data_log = $_data;
   switch ($_action) {
     case 'add':
+      if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+          'msg' => 'access_denied'
+        );
+        return false;
+      }
       switch ($_scope) {
         case 'domain':
           $object = $_data['domain'];
@@ -90,14 +98,6 @@ function policy($_action, $_scope, $_data = null) {
             );
             return false;
           }
-          if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
-              'msg' => 'access_denied'
-            );
-            return false;
-          }
           if ($_data['object_list'] == "bl") {
             $object_list = "blacklist_from";
           }
@@ -151,6 +151,14 @@ function policy($_action, $_scope, $_data = null) {
       }
     break;
     case 'delete':
+      if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
+          'msg' => 'access_denied'
+        );
+        return false;
+      }
       switch ($_scope) {
         case 'domain':
           (array)$prefids = $_data['prefid'];
@@ -215,14 +223,6 @@ function policy($_action, $_scope, $_data = null) {
           else {
             $prefids = $_data['prefid'];
           }
-          if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
-              'msg' => 'access_denied'
-            );
-            return false;
-          }
           foreach ($prefids as $prefid) {
             if (!is_numeric($prefid)) {
               $_SESSION['return'][] = array(

+ 11 - 12
data/web/inc/functions.quarantine.inc.php

@@ -221,13 +221,13 @@ function quarantine($_action, $_data = null) {
             continue;
           }
           $curl = curl_init();
-          curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/rspamd-sock/rspamd.sock');
+          curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
           curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
           curl_setopt($curl, CURLOPT_POST, 1);
           curl_setopt($curl, CURLOPT_TIMEOUT, 30);
-          curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain')); 
+          curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain'));
           curl_setopt($curl, CURLOPT_URL,"http://rspamd/learnspam");
-          curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']); 
+          curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
           $response = curl_exec($curl);
           if (!curl_errno($curl)) {
             $response = json_decode($response, true);
@@ -243,23 +243,22 @@ function quarantine($_action, $_data = null) {
             }
             curl_close($curl);
             $curl = curl_init();
-            curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/rspamd-sock/rspamd.sock');
+            curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
             curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
             curl_setopt($curl, CURLOPT_POST, 1);
             curl_setopt($curl, CURLOPT_TIMEOUT, 30);
-            curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 11')); 
+            curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 11'));
             curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzyadd");
-            curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']); 
+            curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
             $response = curl_exec($curl);
             if (!curl_errno($curl)) {
               $response = json_decode($response, true);
               if (isset($response['error'])) {
                 $_SESSION['return'][] = array(
-                  'type' => 'danger',
+                  'type' => 'warning',
                   'log' => array(__FUNCTION__),
                   'msg' => array('fuzzy_learn_error', $response['error'])
                 );
-                continue;
               }
               curl_close($curl);
               try {
@@ -279,7 +278,7 @@ function quarantine($_action, $_data = null) {
               $_SESSION['return'][] = array(
                 'type' => 'success',
                 'log' => array(__FUNCTION__),
-                'msg' => 'qlearn_spam'
+                'msg' => array('qlearn_spam', $id)
               );
               continue;
             }
@@ -288,7 +287,7 @@ function quarantine($_action, $_data = null) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__),
-                'msg' => array('spam_learn_error', 'curl error ' . curl_errno($curl))
+                'msg' => array('spam_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
               );
               continue;
             }
@@ -301,12 +300,12 @@ function quarantine($_action, $_data = null) {
             continue;
           }
           else {
-            curl_close($curl);
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__),
-              'msg' => array('spam_learn_error', 'curl error ' . curl_errno($curl))
+              'msg' => array('spam_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
             );
+            curl_close($curl);
             continue;
           }
           curl_close($curl);

+ 8 - 0
data/web/inc/functions.ratelimit.inc.php

@@ -5,6 +5,14 @@ function ratelimit($_action, $_scope, $_data = null) {
   $_data_log = $_data;
   switch ($_action) {
     case 'edit':
+      if (!isset($_SESSION['acl']['ratelimit']) || $_SESSION['acl']['ratelimit'] != "1" ) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+          'msg' => 'access_denied'
+        );
+        return false;
+      }
       switch ($_scope) {
         case 'domain':
           if (!is_array($_data['object'])) {

+ 157 - 0
data/web/inc/functions.tls_policy_maps.inc.php

@@ -0,0 +1,157 @@
+<?php
+function tls_policy_maps($_action, $_data = null, $attr = null) {
+	global $pdo;
+	global $lang;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    return false;
+  }
+  switch ($_action) {
+    case 'add':
+      $dest = idn_to_ascii(trim($_data['dest']));
+      $policy = strtolower(trim($_data['policy']));
+      $parameters = (isset($_data['parameters']) && !empty($_data['parameters'])) ? $_data['parameters'] : '';
+      if (!empty($parameters)) {
+        foreach (explode(' ', $parameters) as $parameter) {
+          if (!preg_match('/(.+)\=(.+)/i', $parameter)) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+              'msg' => 'tls_policy_map_parameter_invalid'
+            );
+            return false;
+          }
+        }
+      }
+      $active = intval($_data['active']);
+      $tls_policy_maps = tls_policy_maps('get');
+      foreach ($tls_policy_maps as $tls_policy_map) {
+        if (tls_policy_maps('details', $tls_policy_map)['dest'] == $dest) {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+            'msg' => array('tls_policy_map_entry_exists', htmlspecialchars($dest))
+          );
+          return false;
+        }
+      }
+      $stmt = $pdo->prepare("INSERT INTO `tls_policy_override` (`dest`, `policy`, `parameters`, `active`) VALUES
+        (:dest, :policy, :parameters, :active)");
+      $stmt->execute(array(
+        ':dest' => $dest,
+        ':policy' => $policy,
+        ':parameters' => $parameters,
+        ':active' => $active
+      ));
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+        'msg' => array('tls_policy_map_entry_saved', htmlspecialchars($dest))
+      );
+    break;
+    case 'edit':
+      $ids = (array)$_data['id'];
+      foreach ($ids as $id) {
+        $is_now = tls_policy_maps('details', $id);
+        if (!empty($is_now)) {
+          $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+          $dest = (!empty($_data['dest'])) ? $_data['dest'] : $is_now['dest'];
+          $policy = (!empty($_data['policy'])) ? $_data['policy'] : $is_now['policy'];
+          $parameters = (isset($_data['parameters'])) ? $_data['parameters'] : $is_now['parameters'];
+        }
+        else {
+          $_SESSION['return'][] = array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+            'msg' => 'access_denied'
+          );
+          continue;
+        }
+        if (!empty($parameters)) {
+          foreach (explode(' ', $parameters) as $parameter) {
+            if (!preg_match('/(.+)\=(.+)/i', $parameter)) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+                'msg' => 'tls_policy_map_parameter_invalid'
+              );
+              return false;
+            }
+          }
+        }
+        $tls_policy_maps = tls_policy_maps('get');
+        foreach ($tls_policy_maps as $tls_policy_map) {
+          if ($tls_policy_map == $id) { continue; }
+          if (tls_policy_maps('details', $tls_policy_map)['dest'] == $dest) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+              'msg' => array('recipient_map_entry_exists', htmlspecialchars($dest))
+            );
+            return false;
+          }
+        }
+        $stmt = $pdo->prepare("UPDATE `tls_policy_override` SET
+          `dest` = :dest,
+          `policy` = :policy,
+          `parameters` = :parameters,
+          `active` = :active
+            WHERE `id`= :id");
+        $stmt->execute(array(
+          ':dest' => $dest,
+          ':policy' => $policy,
+          ':parameters' => $parameters,
+          ':active' => $active,
+          ':id' => $id
+        ));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => array('tls_policy_map_entry_saved', htmlspecialchars($dest))
+        );
+      }
+    break;
+    case 'details':
+      $mapdata = array();
+      $id = intval($_data);
+      $stmt = $pdo->prepare("SELECT `id`,
+        `dest`,
+        `policy`,
+        `parameters`,
+        `active` AS `active_int`,
+        CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+        `created`,
+        `modified` FROM `tls_policy_override`
+          WHERE `id` = :id");
+      $stmt->execute(array(':id' => $id));
+      $mapdata = $stmt->fetch(PDO::FETCH_ASSOC);
+      return $mapdata;
+    break;
+    case 'get':
+      $mapdata = array();
+      $all_items = array();
+      $id = intval($_data);
+      $stmt = $pdo->query("SELECT `id` FROM `tls_policy_override`");
+      $all_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      foreach ($all_items as $i) {
+        $mapdata[] = $i['id'];
+      }
+      $all_items = null;
+      return $mapdata;
+    break;
+    case 'delete':
+      $ids = (array)$_data['id'];
+      foreach ($ids as $id) {
+        if (!is_numeric($id)) {
+          return false;
+        }
+        $stmt = $pdo->prepare("DELETE FROM `tls_policy_override` WHERE `id`= :id");
+        $stmt->execute(array(':id' => $id));
+        $_SESSION['return'][] = array(
+          'type' => 'success',
+          'log' => array(__FUNCTION__, $_action, $_data, $_attr),
+          'msg' => array('tls_policy_map_entry_deleted', htmlspecialchars($id))
+        );
+      }
+    break;
+  }
+}

+ 125 - 125
data/web/inc/header.inc.php

@@ -1,145 +1,145 @@
 <!DOCTYPE html>
 <html lang="<?= $_SESSION['mailcow_locale'] ?>">
 <head>
-<meta charset="utf-8">
-<meta http-equiv="X-UA-Compatible" content="IE=edge">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<meta name="theme-color" content="#F5D76E"/>
-<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>
-  <script src="/js/respond.min.js"></script>
-<![endif]-->
-<script src="/js/jquery-1.12.4.min.js"></script>
-<?php if (strtolower(trim($DEFAULT_THEME)) != "lumen"): ?>
-<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/<?= strtolower(trim($DEFAULT_THEME)); ?>/bootstrap.min.css">
-<?php else: ?>
-<link rel="stylesheet" href="/css/bootstrap.min.css">
-<?php endif; ?>
-<link rel="stylesheet" href="/css/breakpoint.min.css">
-<link rel="stylesheet" href="/css/bootstrap-select.min.css">
-<link rel="stylesheet" href="/css/bootstrap-slider.min.css">
-<link rel="stylesheet" href="/css/bootstrap-switch.min.css">
-<link rel="stylesheet" href="/css/footable.bootstrap.min.css">
-<link rel="stylesheet" href="/inc/languages.min.css">
-<link rel="stylesheet" href="/css/mailcow.css">
-<link rel="stylesheet" href="/css/animate.min.css">
-<link rel="stylesheet" href="/css/numberedtextarea.min.css">
-<link rel="stylesheet" href="/css/jquery.jqplot.min.css">
-<?= (preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null; ?>
-<?= (preg_match("/admin.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null; ?>
-<?= (preg_match("/user.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/user.css">' : null; ?>
-<?= (preg_match("/edit.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/edit.css">' : null; ?>
-<?= (preg_match("/quarantine.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/quarantine.css">' : null; ?>
-<?= (preg_match("/debug.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/debug.css">' : null; ?>
-<link rel="shortcut icon" href="/favicon.png" type="image/png">
-<link rel="icon" href="/favicon.png" type="image/png">
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="theme-color" content="#F5D76E"/>
+  <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>
+    <script src="/js/respond.min.js"></script>
+  <![endif]-->
+  <script src="/js/jquery-1.12.4.min.js"></script>
+  <?php if (strtolower(trim($DEFAULT_THEME)) != "lumen"): ?>
+  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.7/<?= strtolower(trim($DEFAULT_THEME)); ?>/bootstrap.min.css">
+  <?php else: ?>
+  <link rel="stylesheet" href="/css/bootstrap.min.css">
+  <?php endif; ?>
+  <link rel="stylesheet" href="/css/breakpoint.min.css">
+  <link rel="stylesheet" href="/css/bootstrap-select.min.css">
+  <link rel="stylesheet" href="/css/bootstrap-slider.min.css">
+  <link rel="stylesheet" href="/css/bootstrap-switch.min.css">
+  <link rel="stylesheet" href="/css/footable.bootstrap.min.css">
+  <link rel="stylesheet" href="/inc/languages.min.css">
+  <link rel="stylesheet" href="/css/mailcow.css">
+  <link rel="stylesheet" href="/css/animate.min.css">
+  <link rel="stylesheet" href="/css/numberedtextarea.min.css">
+  <link rel="stylesheet" href="/css/jquery.jqplot.min.css">
+  <?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null; ?>
+  <?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null; ?>
+  <?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/user.css">' : null; ?>
+  <?= (preg_match("/edit/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/edit.css">' : null; ?>
+  <?= (preg_match("/quarantine/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/quarantine.css">' : null; ?>
+  <?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/debug.css">' : null; ?>
+  <link rel="shortcut icon" href="/favicon.png" type="image/png">
+  <link rel="icon" href="/favicon.png" type="image/png">
 </head>
 <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">
-      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
-        <span class="icon-bar"></span>
-        <span class="icon-bar"></span>
-        <span class="icon-bar"></span>
-      </button>
-      <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>"></a>
-    </div>
-    <div id="navbar" class="navbar-collapse collapse">
-      <ul class="nav navbar-nav navbar-right">
-        <?php
-        if (isset($_SESSION['mailcow_locale'])) {
-        ?>
-        <li class="dropdown<?=(isset($_SESSION['mailcow_locale']) && count($AVAILABLE_LANGUAGES) === 1) ? ' lang-link-disabled"' : '' ?>">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="lang-sm lang-lbl" lang="<?= $_SESSION['mailcow_locale']; ?>"></span><span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
-            <?php
-            foreach ($AVAILABLE_LANGUAGES as $language) {
-            ?>
-            <li<?= ($_SESSION['mailcow_locale'] == $language) ? ' class="active"' : ''; ?>><a href="?<?= http_build_query(array_merge($_GET, array('lang' => $language))); ?>"><span class="lang-xs lang-lbl-full" lang="<?= $language; ?>"></span></a></li>
-            <?php
-            }
-            ?>
-          </ul>
-        </li>
-        <?php
-        }
-        if (isset($_SESSION['mailcow_cc_role'])) {
-        ?>
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?= $lang['header']['mailcow_settings']; ?> <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
-            <?php
-            if (isset($_SESSION['mailcow_cc_role'])) {
-              if ($_SESSION['mailcow_cc_role'] == 'admin') {
-              ?>
-                <li<?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/admin.php"><?= $lang['header']['administration']; ?></a></li>
-                <li<?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/debug.php"><?= $lang['header']['debug']; ?></a></li>
+  <div class="overlay"></div>
+  <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
+    <div class="container-fluid">
+      <div class="navbar-header">
+        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+        </button>
+        <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>"></a>
+      </div>
+      <div id="navbar" class="navbar-collapse collapse">
+        <ul class="nav navbar-nav navbar-right">
+          <?php
+          if (isset($_SESSION['mailcow_locale'])) {
+          ?>
+          <li class="dropdown<?=(isset($_SESSION['mailcow_locale']) && count($AVAILABLE_LANGUAGES) === 1) ? ' lang-link-disabled"' : '' ?>">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="lang-sm lang-lbl" lang="<?= $_SESSION['mailcow_locale']; ?>"></span><span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
               <?php
-              }
-              if ($_SESSION['mailcow_cc_role'] == 'admin' || $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+              foreach ($AVAILABLE_LANGUAGES as $language) {
               ?>
-                <li<?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/mailbox.php"><?= $lang['header']['mailboxes']; ?></a></li>
+              <li<?= ($_SESSION['mailcow_locale'] == $language) ? ' class="active"' : ''; ?>><a href="?<?= http_build_query(array_merge($_GET, array('lang' => $language))); ?>"><span class="lang-xs lang-lbl-full" lang="<?= $language; ?>"></span></a></li>
               <?php
               }
-              if ($_SESSION['mailcow_cc_role'] != 'admin') {
               ?>
-                <li<?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/user.php"><?= $lang['header']['user_settings']; ?></a></li>
+            </ul>
+          </li>
+          <?php
+          }
+          if (isset($_SESSION['mailcow_cc_role'])) {
+          ?>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?= $lang['header']['mailcow_settings']; ?> <span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
               <?php
+              if (isset($_SESSION['mailcow_cc_role'])) {
+                if ($_SESSION['mailcow_cc_role'] == 'admin') {
+                ?>
+                  <li<?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/admin"><?= $lang['header']['administration']; ?></a></li>
+                  <li<?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/debug"><?= $lang['header']['debug']; ?></a></li>
+                <?php
+                }
+                if ($_SESSION['mailcow_cc_role'] == 'admin' || $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+                ?>
+                  <li<?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/mailbox"><?= $lang['header']['mailboxes']; ?></a></li>
+                <?php
+                }
+                if ($_SESSION['mailcow_cc_role'] != 'admin') {
+                ?>
+                  <li<?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/user"><?= $lang['header']['user_settings']; ?></a></li>
+                <?php
+                }
               }
-            }
-            ?>
-          </ul>
-        </li>
-        <?php
-        if (isset($_SESSION['mailcow_cc_role'])) {
-        ?>
-        <li<?= (preg_match("/quarantine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantine.php"><span class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantine']; ?></a></li>
-        <?php
-        }
-        if ($_SESSION['mailcow_cc_role'] == 'admin') {
-        ?>
-        <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
-        }
-        ?>
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="glyphicon glyphicon-link"></span> Apps <span class="caret"></span></a>
-          <ul class="dropdown-menu" role="menu">
+              ?>
+            </ul>
+          </li>
           <?php
-          foreach ($MAILCOW_APPS as $app):
+          if (isset($_SESSION['mailcow_cc_role'])) {
           ?>
-            <li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li>
+          <li<?= (preg_match("/quarantine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantine"><span class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantine']; ?></a></li>
           <?php
-          endforeach;
-          $app_links = customize('get', 'app_links');
-          foreach ($app_links as $row) {
-            foreach ($row as $key => $val):
+          }
+          if ($_SESSION['mailcow_cc_role'] == 'admin') {
           ?>
-            <li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></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
+          }
+          ?>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="glyphicon glyphicon-link"></span> Apps <span class="caret"></span></a>
+            <ul class="dropdown-menu" role="menu">
+            <?php
+            foreach ($MAILCOW_APPS as $app):
+            ?>
+              <li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li>
+            <?php
             endforeach;
+            $app_links = customize('get', 'app_links');
+            foreach ($app_links as $row) {
+              foreach ($row as $key => $val):
+            ?>
+              <li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></a></li>
+            <?php
+              endforeach;
+            }
+            ?>
+            </ul>
+          </li>
+          <?php
           }
+          if (!isset($_SESSION['dual-login']) && isset($_SESSION['mailcow_cc_username'])):
+          ?>
+            <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?></b> <span class="glyphicon glyphicon-log-out"></span></a></li>
+          <?php
+          elseif (isset($_SESSION['dual-login'])):
+          ?>
+            <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?> <span class="text-info">(<?= htmlspecialchars($_SESSION['dual-login']['username']); ?>)</span> </b><span class="glyphicon glyphicon-log-out"></span></a></li>
+          <?php
+          endif;
           ?>
-          </ul>
-        </li>
-        <?php
-        }
-        if (!isset($_SESSION['dual-login']) && isset($_SESSION['mailcow_cc_username'])):
-        ?>
-          <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?></b> <span class="glyphicon glyphicon-log-out"></span></a></li>
-        <?php
-        elseif (isset($_SESSION['dual-login'])):
-        ?>
-          <li class="logged-in-as"><a href="#" onclick="logout.submit()"><b class="username-lia"><?= htmlspecialchars($_SESSION['mailcow_cc_username']); ?> <span class="text-info">(<?= htmlspecialchars($_SESSION['dual-login']['username']); ?>)</span> </b><span class="glyphicon glyphicon-log-out"></span></a></li>
-        <?php
-        endif;
-        ?>
-      </ul>
-    </div><!--/.nav-collapse -->
-  </div><!--/.container-fluid -->
-</nav>
-<form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>
+        </ul>
+      </div><!--/.nav-collapse -->
+    </div><!--/.container-fluid -->
+  </nav>
+  <form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>

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

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "19082018_1004";
+    $db_version = "03102018_1502";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -192,6 +192,26 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "tls_policy_override" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "dest" => "VARCHAR(255) NOT NULL",
+          "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL",
+          "parameters" => "VARCHAR(255) DEFAULT ''",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
+          "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("id")
+          ),
+          "unique" => array(
+            "dest" => array("dest")
+          ),
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "quarantine" => array(
         "cols" => array(
           "id" => "INT NOT NULL AUTO_INCREMENT",
@@ -280,10 +300,7 @@ function init_db_schema() {
           "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
-          "filters" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
-          "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'",
-          "recipient_maps" => "TINYINT(1) NOT NULL DEFAULT '0'",
           ),
         "keys" => array(
           "primary" => array(
@@ -417,6 +434,32 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "da_acl" => array(
+        "cols" => array(
+          "username" => "VARCHAR(255) NOT NULL",
+          "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "login_as" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "filters" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("username")
+          ),
+          "fkey" => array(
+            "fk_domain_admin_acl" => array(
+              "col" => "username",
+              "ref" => "domain_admins.username",
+              "delete" => "CASCADE",
+              "update" => "NO ACTION"
+            )
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "imapsync" => array(
         "cols" => array(
           "id" => "INT NOT NULL AUTO_INCREMENT",
@@ -950,8 +993,9 @@ DELIMITER ;';
       'msg' => 'db_init_complete'
     );
 
-    // Fix user_acl
+    // Fix ACL
     $stmt = $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);");
+    $stmt = $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);");
   }
   catch (PDOException $e) {
     $_SESSION['return'][] = array(

+ 16 - 3
data/web/inc/prerequisites.inc.php

@@ -35,7 +35,7 @@ $hrs = floor($mins / 60);
 $mins -= $hrs * 60;
 $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins);
 
-$dsn = $database_type . ":host=" . $database_host . ";dbname=" . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $opt = [
   PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
   PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
@@ -46,12 +46,22 @@ try {
   $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
 }
 catch (PDOException $e) {
+// Stop when SQL connection fails
 ?>
-<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/>  <?=$e->getMessage();?></center>
+<center style='font-family:sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/>  <?=$e->getMessage();?></center>
 <?php
 exit;
 }
+// Stop when dockerapi is not available
+if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
+?>
+<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
+<?php
+exit;
+}
+
 function pdo_exception_handler($e) {
+    print_r($e);
     if ($e instanceof PDOException) {
       $_SESSION['return'][] = array(
         'type' => 'danger',
@@ -124,6 +134,7 @@ if (isset($_GET['lang']) && in_array($_GET['lang'], $AVAILABLE_LANGUAGES)) {
 require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php';
 include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.acl.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.address_rewriting.inc.php';
@@ -135,12 +146,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rsettings.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
 init_db_schema();
 if (isset($_SESSION['mailcow_cc_role'])) {
-  set_acl();
+  acl('to_session');
 }
 $UI_TEXTS = customize('get', 'ui_texts');
+

+ 1 - 1
data/web/inc/triggers.inc.php

@@ -40,7 +40,7 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 	}
 }
 
-if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['acl']['login_as'] == "1") {
 	if (isset($_GET["duallogin"])) {
     $duallogin = html_entity_decode(rawurldecode($_GET["duallogin"]));
     if (filter_var($duallogin, FILTER_VALIDATE_EMAIL)) {

+ 10 - 0
data/web/inc/vars.inc.php

@@ -9,6 +9,7 @@ This file will be reset on upgrades.
 
 // SQL database connection variables
 $database_type = 'mysql';
+$database_sock = '/var/run/mysqld/mysqld.sock';
 $database_host = 'mysql';
 $database_user = getenv('DBUSER');
 $database_pass = getenv('DBPASS');
@@ -122,3 +123,12 @@ $DOCKER_TIMEOUT = 60;
 
 // Anonymize IPs logged via UI
 $ANONYMIZE_IPS = true;
+
+// Force incoming TLS for new mailboxes by default
+$MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'] = false;
+
+// Force outgoing TLS for new mailboxes by default
+$MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false;
+
+// Force password change on next login (only allows login to mailcow UI)
+$MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;

+ 4 - 4
data/web/index.php

@@ -2,15 +2,15 @@
 require_once 'inc/prerequisites.inc.php';
 
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
-  header('Location: /admin.php');
+  header('Location: /admin');
   exit();
 }
 elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
-  header('Location: /mailbox.php');
+  header('Location: /mailbox');
   exit();
 }
 elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
-  header('Location: /user.php');
+  header('Location: /user');
   exit();
 }
 require_once 'inc/header.inc.php';
@@ -107,6 +107,6 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
     </div>
   </div>
 </div><!-- /.container -->
-<script src="js/index.js"></script>
+<script src="/js/index.js"></script>
 <?php
 require_once 'inc/footer.inc.php';

+ 13 - 9
data/web/js/admin.js

@@ -13,15 +13,19 @@ jQuery(function($){
     e.preventDefault();
     $('#duplicate_dkim_arrow').toggleClass("animation"); 
   });
+  $("#api_legend").on('click', function(e) {
+    e.preventDefault();
+    $('#api_arrow').toggleClass("animation"); 
+  });
   $("#rspamd_preset_1").on('click', function(e) {
     e.preventDefault();
-    $("form[data-id=rsetting]").find("#desc").val(lang.rsettings_preset_1);
-    $("form[data-id=rsetting]").find("#content").val('priority = 10;\nauthenticated = yes;\napply "default" {\n  symbols_enabled = ["DKIM_SIGNED", "RATELIMIT_UPDATE", "RATELIMIT_CHECK", "DYN_RL_CHECK", "HISTORY_SAVE", "MILTER_HEADERS", "ARC_SIGNED"];\n}');
+    $("form[data-id=rsetting]").find("#adminRspamdSettingsDesc").val(lang.rsettings_preset_1);
+    $("form[data-id=rsetting]").find("#adminRspamdSettingsContent").val('priority = 10;\nauthenticated = yes;\napply "default" {\n  symbols_enabled = ["DKIM_SIGNED", "RATELIMIT_UPDATE", "RATELIMIT_CHECK", "DYN_RL_CHECK", "HISTORY_SAVE", "MILTER_HEADERS", "ARC_SIGNED"];\n}');
   });
   $("#rspamd_preset_2").on('click', function(e) {
     e.preventDefault();
-    $("form[data-id=rsetting]").find("#desc").val(lang.rsettings_preset_2);
-    $("form[data-id=rsetting]").find("#content").val('priority = 10;\nrcpt = "/postmaster@.*/";\nwant_spam = yes;');
+    $("form[data-id=rsetting]").find("#adminRspamdSettingsDesc").val(lang.rsettings_preset_2);
+    $("form[data-id=rsetting]").find("#adminRspamdSettingsContent").val('priority = 10;\nrcpt = "/postmaster@.*/";\nwant_spam = yes;');
   });
   $("#dkim_missing_keys").on('click', function(e) {
     e.preventDefault();
@@ -117,15 +121,15 @@ jQuery(function($){
       $.each(data, function (i, item) {
         item.action = '<div class="btn-group">' +
           '<a href="#" data-toggle="modal" id="miau" data-target="#testRelayhostModal" data-relayhost-id="' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-stats"></span> Test</a>' +
-          '<a href="/edit.php?relayhost=' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-          '<a href="#" id="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '<a href="/edit/relayhost/' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
           '</div>';
         item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
       });
     } else if (table == 'forwardinghoststable') {
       $.each(data, function (i, item) {
         item.action = '<div class="btn-group">' +
-          '<a href="#" id="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
           '</div>';
         if (item.keep_spam == "yes") {
           item.keep_spam = lang.no;
@@ -140,8 +144,8 @@ jQuery(function($){
         item.selected_domains = escapeHtml(item.selected_domains.toString().replace(/,/g, " "));
         item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
         item.action = '<div class="btn-group">' +
-          '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-          '<a href="#" id="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
           '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' +
           '</div>';
       });

+ 6 - 5
data/web/js/api.js

@@ -5,9 +5,9 @@ $(document).ready(function() {
     } else {
       var parent_btn_grp = $(elem).parentsUntil(".btn-group").parent();
       if (parent_btn_grp.hasClass('btn-group')) {
-        parent_btn_grp.replaceWith('<button class="btn btn-default btn-sm" disabled>' + loading_text + '</a>');
+        parent_btn_grp.replaceWith('<button class="btn btn-default btn-sm" disabled>' + lang_footer.loading + '</a>');
       }
-      $(elem).text(loading_text);
+      $(elem).text(lang_footer.loading);
       $(elem).attr('data-submitted', '1');
       function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
       $(document).on("keydown", disableF5);
@@ -71,7 +71,7 @@ $(document).ready(function() {
   });
 
   // General API edit actions
-  $(document).on('click', '#edit_selected', function(e) {
+  $(document).on('click', "[data-action='edit_selected']", function(e) {
     e.preventDefault();
     var id = $(this).data('id');
     var api_url = $(this).data('api-url');
@@ -159,7 +159,7 @@ $(document).ready(function() {
   });
 
   // General API add actions
-  $(document).on('click', '#add_item', function(e) {
+  $(document).on('click', "[data-action='add_item']", function(e) {
     e.preventDefault();
     var id = $(this).data('id');
     var api_url = $(this).data('api-url');
@@ -252,7 +252,7 @@ $(document).ready(function() {
   });
 
   // General API delete actions
-  $(document).on('click', '#delete_selected', function(e) {
+  $(document).on('click', "[data-action='delete_selected']", function(e) {
     e.preventDefault();
     var id = $(this).data('id');
     // If clicked element #delete_selected has data-item attribute, it is added to "items"
@@ -283,6 +283,7 @@ $(document).ready(function() {
         keyboard: false
       })
       .one('click', '#IsConfirmed', function(e) {
+        if (is_active($('#IsConfirmed'))) { return false; }
         $.ajax({
           type: "POST",
           dataType: "json",

+ 25 - 19
data/web/js/edit.js

@@ -10,37 +10,43 @@ $(document).ready(function() {
   });
   $("#disable_sender_check").click(function( event ) {
     if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
-      $('#sender_acl').prop('disabled', true);
-      $('#sender_acl').selectpicker('refresh');
+      $('#editSelectSenderACL').prop('disabled', true);
+      $('#editSelectSenderACL').selectpicker('refresh');
     }
     else {
-      $('#sender_acl').prop('disabled', false);
-      $('#sender_acl').selectpicker('refresh');
+      $('#editSelectSenderACL').prop('disabled', false);
+      $('#editSelectSenderACL').selectpicker('refresh');
     }
   });
   if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
     $('#textarea_alias_goto').prop('disabled', true);
   }
+
   $("#script_data").numberedtextarea({allowTabChar: true});
+
+  $("#mailbox-password-warning-close").click(function( event ) {
+    $('#mailbox-passwd-hidden-info').addClass('hidden');
+    $('#mailbox-passwd-form-groups').removeClass('hidden');
+  });
 });
-if ($("#multiple_bookings_select").val() == "custom") {
+if ($("#editSelectMultipleBookings").val() == "custom") {
   $("#multiple_bookings_custom_div").show();
-  $("#multiple_bookings").val($("#multiple_bookings_custom").val());
+  $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
 }
-$("#multiple_bookings_select").change(function() {
-  $("#multiple_bookings").val($("#multiple_bookings_select").val());
-  if ($("#multiple_bookings").val() == "custom") {
+$("#editSelectMultipleBookings").change(function() {
+  $('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
+  if ($('input[name=multiple_bookings]').val() == "custom") {
     $("#multiple_bookings_custom_div").show();
   }
   else {
     $("#multiple_bookings_custom_div").hide();
   }
 });
-if ($("#sender_acl option[value='\*']:selected").length > 0){
+if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
   $("#sender_acl_disabled").show();
 }
-$('#sender_acl').change(function() {
-  if ($("#sender_acl option[value='\*']:selected").length > 0){
+$('#editSelectSenderACL').change(function() {
+  if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
     $("#sender_acl_disabled").show();
   }
   else {
@@ -48,7 +54,7 @@ $('#sender_acl').change(function() {
   }
 });
 $("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
-  $("#multiple_bookings").val($("#multiple_bookings_custom").val());
+  $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
 });
 jQuery(function($){
   // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
@@ -61,10 +67,10 @@ jQuery(function($){
       "columns": [
         {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
         {"name":"prefid","style":{"maxWidth":"40px","width":"40px"},"title":"ID","filterable": false,"sortable": false},
-        {"sorted": true,"name":"value","title":lang.spamfilter_table_rule},
+        {"sorted": true,"name":"value","title":lang_user.spamfilter_table_rule},
         {"name":"object","title":"Scope"}
       ],
-      "empty": lang.empty,
+      "empty": lang_user.empty,
       "rows": $.ajax({
         dataType: 'json',
         url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
@@ -78,7 +84,7 @@ jQuery(function($){
               item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
             }
             else {
-              item.chkbox = '<input type="checkbox" disabled title="' + lang.spamfilter_table_domain_policy + '" />';
+              item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
             }
           });
         }
@@ -98,10 +104,10 @@ jQuery(function($){
       "columns": [
         {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
         {"name":"prefid","style":{"maxWidth":"40px","width":"40px"},"title":"ID","filterable": false,"sortable": false},
-        {"sorted": true,"name":"value","title":lang.spamfilter_table_rule},
+        {"sorted": true,"name":"value","title":lang_user.spamfilter_table_rule},
         {"name":"object","title":"Scope"}
       ],
-      "empty": lang.empty,
+      "empty": lang_user.empty,
       "rows": $.ajax({
         dataType: 'json',
         url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
@@ -115,7 +121,7 @@ jQuery(function($){
               item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
             }
             else {
-              item.chkbox = '<input type="checkbox" disabled tooltip="' + lang.spamfilter_table_domain_policy + '" />';
+              item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
             }
           });
         }

+ 108 - 31
data/web/js/mailbox.js

@@ -1,11 +1,11 @@
 $(document).ready(function() {
+  acl_data = JSON.parse(acl);
   FooTable.domainFilter = FooTable.Filtering.extend({
     construct: function(instance){
       this._super(instance);
       var domain_list = [];
       $.ajax({
         dataType: 'json',
-        'async': false,
         url: '/api/v1/get/domain/all',
         jsonp: false,
         error: function () {
@@ -82,11 +82,12 @@ $(document).ready(function() {
 
   $(".generate_password").click(function( event ) {
     event.preventDefault();
+    $('[data-hibp]').trigger('input');
     var random_passwd = Math.random().toString(36).slice(-8)
-    $('#password').prop('type', 'text');
-    $('#password').val(random_passwd);
-    $('#password2').prop('type', 'text');
-    $('#password2').val(random_passwd);
+    $(this).closest("form").find("input[name='password']").prop('type', 'text');
+    $(this).closest("form").find("input[name='password2']").prop('type', 'text');
+    $(this).closest("form").find("input[name='password']").val(random_passwd);
+    $(this).closest("form").find("input[name='password2']").val(random_passwd);
   });
 
   $(".goto_checkbox").click(function( event ) {
@@ -233,6 +234,13 @@ jQuery(function($){
     eval(draw_table + '()');
   });
   function table_mailbox_ready(ft, name) {
+    if(is_dual) {
+      $('.login_as').data("toggle", "tooltip")
+        .attr("disabled", true)
+        .removeAttr("href")
+        .attr("title", "Dual login cannot be used twice")
+        .tooltip();
+    }
     heading = ft.$el.parents('.tab-pane').find('.panel-heading')
     var ft_paging = ft.use(FooTable.Paging)
     $(heading).children('.table-lines').text(function(){
@@ -264,10 +272,10 @@ jQuery(function($){
         },
         },
         {"name":"max_quota_for_mbox","title":lang.mailbox_quota,"breakpoints":"xs sm","style":{"width":"125px"}},
-        {"name":"rl","title":"RL","breakpoints":"xs sm md","style":{"width":"125px"}},
-        {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm"},
+        {"name":"rl","title":"RL","breakpoints":"xs sm md","style":{"maxWidth":"100px","width":"100px"}},
+        {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md"},
         {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
-        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"}
       ],
       "rows": $.ajax({
         dataType: 'json',
@@ -292,11 +300,11 @@ jQuery(function($){
             item.chkbox = '<input type="checkbox" data-id="domain" name="multi_select" value="' + encodeURIComponent(item.domain_name) + '" />';
             item.action = '<div class="btn-group">';
             if (role == "admin") {
-              item.action += '<a href="/edit.php?domain=' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-                '<a href="#" id="delete_selected" data-id="single-domain" data-api-url="delete/domain" data-item="' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>';
+              item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-domain" data-api-url="delete/domain" data-item="' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>';
             }
             else {
-              item.action += '<a href="/edit.php?domain=' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>';
+              item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>';
             }
             item.action += '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.domain_name) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>';
           });
@@ -345,6 +353,8 @@ jQuery(function($){
         },
         },
         {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"},
+        {"name":"tls_enforce_in","filterable": false,"title":lang.tls_enforce_in,"breakpoints":"all"},
+        {"name":"tls_enforce_out","filterable": false,"title":lang.tls_enforce_out,"breakpoints":"all"},
         {"name":"in_use","filterable": false,"type":"html","title":lang.in_use,"sortValue": function(value){
           return Number($(value).find(".progress-bar").attr('aria-valuenow'));
         },
@@ -374,17 +384,19 @@ jQuery(function($){
               }).join('/1');
             }
             item.chkbox = '<input type="checkbox" data-id="mailbox" name="multi_select" value="' + encodeURIComponent(item.username) + '" />';
-            if (role == "admin") {
+            item.tls_enforce_in = '<span class="text-' + (item.attributes.tls_enforce_in == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>';
+            item.tls_enforce_out = '<span class="text-' + (item.attributes.tls_enforce_out == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>';
+            if (acl_data.login_as === 1) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?mailbox=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
-              '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' +
+              '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="login_as btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' +
               '</div>';
             }
             else {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?mailbox=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             }
             item.in_use = '<div class="progress">' +
@@ -404,6 +416,7 @@ jQuery(function($){
         "delay": 100,
         "position": "left",
         "connectors": false,
+        //"container": "#tab-mailboxes.panel",
         "placeholder": lang.filter_table
       },
       "components": {
@@ -451,8 +464,8 @@ jQuery(function($){
               item.multiple_bookings = '<span id="active-script" class="label label-danger">' + lang.booking_custom_short + ' (' + item.multiple_bookings + ')</span>';
             }
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?resource=' + encodeURIComponent(item.name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-resource" data-api-url="delete/resource" data-item="' + item.name + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/resource/' + encodeURIComponent(item.name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-resource" data-api-url="delete/resource" data-item="' + item.name + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />';
             item.name = escapeHtml(item.name);
@@ -510,8 +523,8 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?bcc=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-bcc" data-api-url="delete/bcc" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/bcc/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-bcc" data-api-url="delete/bcc" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="bcc" name="multi_select" value="' + item.id + '" />';
             item.local_dest = escapeHtml(item.local_dest);
@@ -573,8 +586,8 @@ jQuery(function($){
               item.recipient_map_old = escapeHtml(item.recipient_map_old);
               item.recipient_map_new = escapeHtml(item.recipient_map_new);
               item.action = '<div class="btn-group">' +
-                '<a href="/edit.php?recipient_map=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-                '<a href="#" id="delete_selected" data-id="single-recipient_map" data-api-url="delete/recipient_map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+                '<a href="/edit/recipient_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-recipient_map" data-api-url="delete/recipient_map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
                 '</div>';
               item.chkbox = '<input type="checkbox" data-id="recipient_map" name="multi_select" value="' + item.id + '" />';
             });
@@ -606,6 +619,69 @@ jQuery(function($){
       }
     });
   }
+  function draw_tls_policy_table() {
+    ft_tls_policy_table = FooTable.init('#tls_policy_table', {
+      "columns": [
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
+        {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
+        {"name":"dest","title":lang.tls_map_dest},
+        {"name":"policy","title":lang.tls_map_policy},
+        {"name":"parameters","title":lang.tls_map_parameters},
+        {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"}
+      ],
+      "empty": lang.empty,
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/tls-policy-map/all',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw tls policy map table');
+        },
+        success: function (data) {
+          if (role == "admin") {
+            $.each(data, function (i, item) {
+              item.dest = escapeHtml(item.dest);
+              item.policy = '<b>' + escapeHtml(item.policy) + '</b>';
+              if (item.parameters == '') {
+                item.parameters = '<code>-</code>';
+              } else {
+                item.parameters = '<code>' + escapeHtml(item.parameters) + '</code>';
+              }
+              item.action = '<div class="btn-group">' +
+                '<a href="/edit/tls_policy_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-tls-policy-map" data-api-url="delete/tls-policy-map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+                '</div>';
+              item.chkbox = '<input type="checkbox" data-id="tls-policy-map" name="multi_select" value="' + item.id + '" />';
+            });
+          }
+        }
+      }),
+      "paging": {
+        "enabled": true,
+        "limit": 5,
+        "size": pagination_size
+      },
+      "filtering": {
+        "enabled": true,
+        "delay": 100,
+        "position": "left",
+        "connectors": false,
+        "placeholder": lang.filter_table
+      },
+      "sorting": {
+        "enabled": true
+      },
+      "on": {
+        "ready.ft.table": function(e, ft){
+          table_mailbox_ready(ft, 'tls_policy_table');
+        },
+        "after.ft.paging": function(e, ft){
+          paging_mailbox_after(ft, 'tls_policy_table');
+        }
+      }
+    });
+  }
   function draw_alias_table() {
     ft_alias_table = FooTable.init('#alias_table', {
       "columns": [
@@ -628,8 +704,8 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?alias=' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-alias" data-api-url="delete/alias" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/alias/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-alias" data-api-url="delete/alias" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
             item.goto = escapeHtml(item.goto.replace(/,/g, " "));
@@ -703,8 +779,8 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?aliasdomain=' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-alias-domain" data-api-url="delete/alias-domain" data-item="' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/aliasdomain/' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-alias-domain" data-api-url="delete/alias-domain" data-item="' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.alias_domain) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="alias-domain" name="multi_select" value="' + encodeURIComponent(item.alias_domain) + '" />';
@@ -771,8 +847,8 @@ jQuery(function($){
             }
             item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1;
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?syncjob=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
             if (item.is_running == 1) {
@@ -842,8 +918,8 @@ jQuery(function($){
             item.script_data = '<pre style="margin:0px">' + escapeHtml(item.script_data) + '</pre>'
             item.filter_type = '<div class="label label-default">' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '</div>'
             item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?filter=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="/edit/filter/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="filter_item" name="multi_select" value="' + item.id + '" />'
           });
@@ -884,5 +960,6 @@ jQuery(function($){
   draw_filter_table();
   draw_bcc_table();
   draw_recipient_map_table();
+  draw_tls_policy_table();
 
 });

+ 193 - 0
data/web/js/mailcow.js

@@ -0,0 +1,193 @@
+$(document).ready(function() {
+  // mailcow alert box generator
+  window.mailcow_alert_box = function(message, type) {
+    msg = $('<span/>').text(message).text();
+    if (type == 'danger') {
+      auto_hide = 0;
+      $('#' + localStorage.getItem("add_modal")).modal('show');
+      localStorage.removeItem("add_modal");
+    } else {
+      auto_hide = 5000;
+    }
+    $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
+  }
+
+  // https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate
+  function shake(div,interval=100,distance=10,times=4) {
+    $(div).css('position','relative');
+    for(var iter=0;iter<(times+1);iter++){
+      $(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
+    }
+    $(div).animate({ left: 0},interval);
+  }
+
+  // form cache
+  $('[data-cached-form="true"]').formcache({key: $(this).data('id')});
+
+  //  tooltips
+  $(function () {
+    $('[data-toggle="tooltip"]').tooltip()
+  });
+
+  // remember last navigation pill
+  (function () {
+    'use strict';
+    if ($('a[data-toggle="tab"]').length) {
+      $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+        if ($(this).data('dont-remember') == 1) {
+          return true;
+        }
+        var id = $(this).parents('[role="tablist"]').attr('id');
+        var key = 'lastTag';
+        if (id) {
+          key += ':' + id;
+        }
+        localStorage.setItem(key, $(e.target).attr('href'));
+      });
+      $('[role="tablist"]').each(function (idx, elem) {
+        var id = $(elem).attr('id');
+        var key = 'lastTag';
+        if (id) {
+          key += ':' + id;
+        }
+        var lastTab = localStorage.getItem(key);
+        if (lastTab) {
+          $('[href="' + lastTab + '"]').tab('show');
+        }
+      });
+    }
+  })();
+
+  // IE fix to hide scrollbars when table body is empty
+  $('tbody').filter(function (index) {
+    return $(this).children().length < 1;
+  }).remove();
+
+  // selectpicker
+  $('select').selectpicker();
+
+  // haveibeenpwned?
+  $('[data-hibp]').after('<p class="small haveibeenpwned">↪ Check against haveibeenpwned.com</p><span class="hibp-out"></span>');
+  $('[data-hibp]').on('input', function() {
+    out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');
+  });
+  $('.haveibeenpwned:not(.task-running)').on('click', function() {
+    var hibp_field = $(this)
+    $(hibp_field).addClass('task-running');
+    var hibp_result = $(hibp_field).next('.hibp-out')
+    var password_field = $(this).prev('[data-hibp]')
+    if ($(password_field).val() == '') {
+      shake(password_field);
+    }
+    else {
+      $(hibp_result).attr('class', 'hibp-out label label-info');
+      $(hibp_result).text(lang_footer.loading);
+      var password_digest = $.sha1($(password_field).val())
+      var digest_five = password_digest.substring(0, 5).toUpperCase();
+      var queryURL = "https://api.pwnedpasswords.com/range/" + digest_five;
+      var compl_digest = password_digest.substring(5, 41).toUpperCase();
+      $.ajax({
+        url: queryURL,
+        type: 'GET',
+        success: function(res) {
+          if (res.search(compl_digest) > -1){
+            $(hibp_result).removeClass('label label-info').addClass('label label-danger');
+            $(hibp_result).text(lang_footer.hibp_nok)
+          } else {
+            $(hibp_result).removeClass('label label-info').addClass('label label-success');
+            $(hibp_result).text(lang_footer.hibp_ok)
+          }
+          $(hibp_field).removeClass('task-running');
+        },
+        error: function(xhr, status, error) {
+          $(hibp_result).removeClass('label label-info').addClass('label label-warning');
+          $(hibp_result).text('API error: ' + xhr.responseText)
+          $(hibp_field).removeClass('task-running');
+        }
+      });
+    }
+  });
+
+  // Disable disallowed inputs
+  $('[data-acl="0"]').each(function(){
+    if ($(this).hasClass('btn-group')) {
+      $(this).find('a').each(function(){
+        $(this).removeClass('dropdown-toggle')
+          .removeAttr('data-toggle')
+          .removeAttr('id')
+          .attr("disabled", true);
+        $(this).click(function(event) {
+          event.preventDefault();
+          return;
+        });
+      });
+      $(this).find('button').each(function() {
+        $(this).attr("disabled", true);
+      });
+    } else if ($(this).hasClass('input-group')) {
+      $(this).find('input').each(function() {
+        $(this).removeClass('dropdown-toggle')
+          .removeAttr('data-toggle')
+          .attr("disabled", true);
+        $(this).click(function(event) {
+          event.preventDefault();
+        });
+      });
+      $(this).find('button').each(function() {
+        $(this).attr("disabled", true);
+      });
+    } else if ($(this).hasClass('btn')) {
+      $(this).attr("disabled", true);
+    } else if ($(this).attr('data-provide', 'slider')) {
+      $(this).slider("disable");
+    }
+    $(this).data("toggle", "tooltip");
+    $(this).attr("title", lang_acl.prohibited);
+    $(this).tooltip(); 
+  });
+
+  // disable submit after submitting form (not API driven buttons)
+  $('form').submit(function() {
+    if ($('form button[type="submit"]').data('submitted') == '1') {
+      return false;
+    } else {
+      $(this).find('button[type="submit"]').first().text(lang_footer.loading);
+      $('form button[type="submit"]').attr('data-submitted', '1');
+      function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
+      $(document).on("keydown", disableF5);
+    }
+  });
+
+  // trigger container restart
+  $('#RestartContainer').on('show.bs.modal', function(e) {
+    var container = $(e.relatedTarget).data('container');
+    $('#containerName').text(container);
+    $('#triggerRestartContainer').click(function(){
+      $(this).prop("disabled",true);
+      $(this).html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> ');
+      $('#statusTriggerRestartContainer').html(lang_footer.restarting_container);
+      $.ajax({
+        method: 'get',
+        url: '/inc/ajax/container_ctrl.php',
+        timeout: docker_timeout,
+        data: {
+        'service': container,
+        'action': 'restart'
+        }
+      })
+      .always( function (data, status) {
+        $('#statusTriggerRestartContainer').append(data);
+        var htmlResponse = $.parseHTML(data)
+        if ($(htmlResponse).find('span').hasClass('text-success')) {
+          $('#triggerRestartContainer').html('<span class="glyphicon glyphicon-ok"></span> ');
+          setTimeout(function(){
+            $('#RestartContainer').modal('toggle'); 
+            window.location = window.location.href.split("#")[0];
+          }, 1200);
+        } else {
+          $('#triggerRestartContainer').html('<span class="glyphicon glyphicon-remove"></span> ');
+        }
+      })
+    });
+  })
+});

+ 1 - 1
data/web/js/quarantine.js

@@ -28,7 +28,7 @@ jQuery(function($){
           $.each(data, function (i, item) {
             item.action = '<div class="btn-group">' +
               '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><span class="glyphicon glyphicon-modal-window"></span> ' + lang.show_item + '</a>' +
-              '<a href="#" id="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
           });

+ 1 - 0
data/web/js/sha1.min.js

@@ -0,0 +1 @@
+!function(r){var o=function(r,o){return r<<o|r>>>32-o},e=function(r){var o,e="";for(o=7;o>=0;o--)e+=(r>>>4*o&15).toString(16);return e};jQuery.extend({sha1:function(r){var a,t,n,h,C,c,f,d,u,i=new Array(80),A=1732584193,g=4023233417,s=2562383102,S=271733878,m=3285377520,p=(r=function(r){r=r.replace(/\x0d\x0a/g,"\n");for(var o="",e=0;e<r.length;e++){var a=r.charCodeAt(e);a<128?o+=String.fromCharCode(a):a>127&&a<2048?(o+=String.fromCharCode(a>>6|192),o+=String.fromCharCode(63&a|128)):(o+=String.fromCharCode(a>>12|224),o+=String.fromCharCode(a>>6&63|128),o+=String.fromCharCode(63&a|128))}return o}(r)).length,l=new Array;for(t=0;t<p-3;t+=4)n=r.charCodeAt(t)<<24|r.charCodeAt(t+1)<<16|r.charCodeAt(t+2)<<8|r.charCodeAt(t+3),l.push(n);switch(p%4){case 0:t=2147483648;break;case 1:t=r.charCodeAt(p-1)<<24|8388608;break;case 2:t=r.charCodeAt(p-2)<<24|r.charCodeAt(p-1)<<16|32768;break;case 3:t=r.charCodeAt(p-3)<<24|r.charCodeAt(p-2)<<16|r.charCodeAt(p-1)<<8|128}for(l.push(t);l.length%16!=14;)l.push(0);for(l.push(p>>>29),l.push(p<<3&4294967295),a=0;a<l.length;a+=16){for(t=0;t<16;t++)i[t]=l[a+t];for(t=16;t<=79;t++)i[t]=o(i[t-3]^i[t-8]^i[t-14]^i[t-16],1);for(h=A,C=g,c=s,f=S,d=m,t=0;t<=19;t++)u=o(h,5)+(C&c|~C&f)+d+i[t]+1518500249&4294967295,d=f,f=c,c=o(C,30),C=h,h=u;for(t=20;t<=39;t++)u=o(h,5)+(C^c^f)+d+i[t]+1859775393&4294967295,d=f,f=c,c=o(C,30),C=h,h=u;for(t=40;t<=59;t++)u=o(h,5)+(C&c|C&f|c&f)+d+i[t]+2400959708&4294967295,d=f,f=c,c=o(C,30),C=h,h=u;for(t=60;t<=79;t++)u=o(h,5)+(C^c^f)+d+i[t]+3395469782&4294967295,d=f,f=c,c=o(C,30),C=h,h=u;A=A+h&4294967295,g=g+C&4294967295,s=s+c&4294967295,S=S+f&4294967295,m=m+d&4294967295}return(u=e(A)+e(g)+e(s)+e(S)+e(m)).toLowerCase()}})}();

+ 3 - 3
data/web/js/user.js

@@ -64,7 +64,7 @@ jQuery(function($){
           $.each(data, function (i, item) {
             if (acl_data.spam_alias === 1) {
               item.action = '<div class="btn-group">' +
-                '<a href="#" id="delete_selected" data-id="single-tla" data-api-url="delete/time_limited_alias" data-item="' + encodeURIComponent(item.address) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-tla" data-api-url="delete/time_limited_alias" data-item="' + encodeURIComponent(item.address) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
                 '</div>';
               item.chkbox = '<input type="checkbox" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />';
               item.address = escapeHtml(item.address);
@@ -122,8 +122,8 @@ jQuery(function($){
             item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1);
             if (acl_data.syncjobs === 1) {
               item.action = '<div class="btn-group">' +
-                '<a href="/edit.php?syncjob=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-                '<a href="#" id="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+                '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
                 '</div>';
               item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
             }

+ 41 - 1
data/web/json_api.php

@@ -153,6 +153,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "recipient_map":
             process_add_return(recipient_map('add', $attr));
           break;
+          case "tls-policy-map":
+            process_add_return(tls_policy_maps('add', $attr));
+          break;
         }
       break;
       case "get":
@@ -164,7 +167,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
             switch ($object) {
               case "actions":
                 $curl = curl_init();
-                curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/rspamd-sock/rspamd.sock');
+                curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
                 curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
                 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
                 $data = curl_exec($curl);
@@ -662,6 +665,31 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               break;
             }
           break;
+          case "tls-policy-map":
+            switch ($object) {
+              case "all":
+                $tls_policy_maps_items = tls_policy_maps('get');
+                if (!empty($tls_policy_maps_items)) {
+                  foreach ($tls_policy_maps_items as $tls_policy_maps_item) {
+                    if ($details = tls_policy_maps('details', $tls_policy_maps_item)) {
+                      $data[] = $details;
+                    }
+                    else {
+                      continue;
+                    }
+                  }
+                }
+                process_get_return($data);
+              break;
+              default:
+                $data = tls_policy_maps('details', $object);
+                if (!empty($data)) {
+                  $data[] = $details;
+                }
+                process_get_return($data);
+              break;
+            }
+          break;
           case "policy_wl_mailbox":
             switch ($object) {
               default:
@@ -919,6 +947,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "recipient_map":
             process_delete_return(recipient_map('delete', array('id' => $items)));
           break;
+          case "tls-policy-map":
+            process_delete_return(tls_policy_maps('delete', array('id' => $items)));
+          break;
           case "fwdhost":
             process_delete_return(fwdhost('delete', array('forwardinghost' => $items)));
           break;
@@ -991,6 +1022,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "recipient_map":
             process_edit_return(recipient_map('edit', array_merge(array('id' => $items), $attr)));
           break;
+          case "tls-policy-map":
+            process_edit_return(tls_policy_maps('edit', array_merge(array('id' => $items), $attr)));
+          break;
           case "alias":
             process_edit_return(mailbox('edit', 'alias', array_merge(array('id' => $items), $attr)));
           break;
@@ -1039,6 +1073,12 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "rl-mbox":
             process_edit_return(ratelimit('edit', 'mailbox', array_merge(array('object' => $items), $attr)));
           break;
+          case "user-acl":
+            process_edit_return(acl('edit', 'user', array_merge(array('username' => $items), $attr)));
+          break;
+          case "da-acl":
+            process_edit_return(acl('edit', 'domainadmin', array_merge(array('username' => $items), $attr)));
+          break;
           case "alias-domain":
             process_edit_return(mailbox('edit', 'alias_domain', array_merge(array('alias_domain' => $items), $attr)));
           break;

+ 44 - 8
data/web/lang/lang.de.php

@@ -16,6 +16,9 @@ $lang['footer']['delete_these_items'] = 'Sind Sie sicher, dass die Änderungen a
 $lang['footer']['delete_now'] = 'Jetzt löschen';
 $lang['footer']['cancel'] = 'Abbrechen';
 
+$lang['footer']['hibp_nok'] = 'Übereinstimmung gefunden! Dieses Passwort ist potentiell gefährlich!';
+$lang['footer']['hibp_ok'] = 'Keine Übereinstimmung gefunden.';
+
 $lang['danger']['mysql_error'] = "MySQL Fehler: %s";
 $lang['danger']['redis_error'] = "Redis Fehler: %s";
 $lang['danger']['unknown_tfa_method'] = "Unbekannte TFA Methode";
@@ -43,6 +46,7 @@ $lang['danger']['domain_cannot_match_hostname'] = "Domain darf nicht dem Hostnam
 $lang['warning']['domain_added_sogo_failed'] = "Domain wurde hinzugefügt; SOGo konnte nicht neugestartet werden";
 $lang['danger']['rl_timeframe'] = "Ratelimit Zeitraum ist inkorrekt";
 $lang['success']['rl_saved'] = "Ratelimit für Objekt %s wurde gesetzt";
+$lang['success']['acl_saved'] = "ACL für Objekt %s wurde gesetzt";
 $lang['success']['deleted_syncjobs'] = "Syncjobs gelöscht: %s";
 $lang['success']['deleted_syncjob'] = "Syncjobs ID %s gelöscht";
 $lang['success']['delete_filters'] = "Filter gelöscht: %s";
@@ -236,6 +240,17 @@ $lang['header']['mailcow_settings'] = 'Konfiguration';
 $lang['header']['administration'] = 'Administration';
 $lang['header']['mailboxes'] = 'Mailboxen';
 $lang['header']['user_settings'] = 'Benutzereinstellungen';
+$lang['mailbox']['tls_policy_maps'] = 'TLS-Richtlinien';
+$lang['mailbox']['tls_policy_maps_long'] = 'Ausgehende TLS-Richtlinien';
+$lang['mailbox']['tls_policy_maps_info'] = 'Nachstehende Richtlinien erzwingen TLS-Transportregeln unabhängig von TLS-Richtlinieneinstellungen eines Benutzers.<br>
+  Für weitere Informationen zur Syntax sollte <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">die "smtp_tls_policy_maps" Dokumentation</a> konsultiert werden.';
+$lang['mailbox']['tls_enforce_in'] = 'Enforce TLS incoming';
+$lang['mailbox']['tls_enforce_out'] = 'Enforce TLS outgoing';
+$lang['mailbox']['tls_map_dest'] = 'Ziel';
+$lang['mailbox']['tls_map_dest_info'] = 'Beispiele: example.org, .example.org, mail@example.org, [mail.example.org]:25';
+$lang['mailbox']['tls_map_policy'] = 'Richtlinie';
+$lang['mailbox']['tls_map_parameters'] = 'Parameter';
+$lang['mailbox']['tls_map_parameters_info'] = 'Leer oder Parameter, Beispiele: protocols=!SSLv2 ciphers=medium exclude=3DES';
 $lang['mailbox']['booking_0'] = 'Immer als verfügbar anzeigen';
 $lang['mailbox']['booking_lt0'] = 'Unbegrenzt, jedoch anzeigen, wenn gebucht';
 $lang['mailbox']['booking_custom'] = 'Benutzerdefiniertes Limit';
@@ -332,6 +347,8 @@ $lang['edit']['relay_all_info'] = '<small>Wenn Sie <b>nicht</b> alle Empfänger-
 $lang['edit']['full_name'] = 'Voller Name';
 $lang['edit']['quota_mb'] = 'Speicherplatz (MiB)';
 $lang['edit']['sender_acl'] = 'Darf Nachrichten versenden als';
+$lang['edit']['sender_acl_disabled'] = '↳ <span class="label label-danger">Absenderprüfung deaktiviert</span>';
+$lang['user']['sender_acl_disabled'] = '<span class="label label-danger">Absenderprüfung deaktiviert</span>';
 $lang['edit']['previous'] = 'Vorherige Seite';
 $lang['edit']['unchanged_if_empty'] = 'Unverändert, wenn leer';
 $lang['edit']['dont_check_sender_acl'] = 'Absender für Domain %s u. Alias-Dom. nicht prüfen';
@@ -339,7 +356,22 @@ $lang['edit']['multiple_bookings'] = 'Mehrfaches Buchen';
 $lang['edit']['kind'] = 'Art';
 $lang['edit']['resource'] = 'Ressource';
 
-$lang['add']['syncjob'] = 'Sync-Job erstellen';
+$lang['acl']['spam_alias'] = 'Temporäre E-Mail Aliasse';
+$lang['acl']['tls_policy'] = 'Verschlüsselungsrichtlinie';
+$lang['acl']['spam_score'] = 'Spam Bewertung';
+$lang['acl']['spam_policy'] = 'Blacklist/Whitelist';
+$lang['acl']['delimiter_action'] = 'Delimiter Aktionen (tags)';
+$lang['acl']['syncjobs'] = 'Sync Jobs';
+$lang['acl']['eas_reset'] = 'EAS-Cache zurücksetzen';
+$lang['acl']['quarantine'] = 'Quarantäne';
+$lang['acl']['login_as'] = 'Einloggen als Mailbox-Benutzer';
+$lang['acl']['bcc_maps'] = 'BCC Maps';
+$lang['acl']['filters'] = 'Filter';
+$lang['acl']['ratelimit'] = 'Rate limit';
+$lang['acl']['recipient_maps'] = 'Empfängerumschreibungen';
+$lang['acl']['prohibited'] = 'Untersagt durch Richtlinie';
+
+$lang['add']['generate'] = 'generieren';
 $lang['add']['syncjob_hint'] = 'Passwörter werden unverschlüsselt abgelegt!';
 $lang['add']['hostname'] = 'Servername';
 $lang['add']['port'] = 'Port';
@@ -597,7 +629,7 @@ $lang['quarantine']['subj'] = "Betreff";
 $lang['quarantine']['text_plain_content'] = "Inhalt (text/plain)";
 $lang['quarantine']['text_from_html_content'] = "Inhalt (html, konvertiert)";
 $lang['quarantine']['atts'] = "Anhänge";
-$lang['danger']['fuzzy_learn_error'] = "Fuzzy Lernfehler: %s";
+$lang['warning']['fuzzy_learn_error'] = "Fuzzy Lernfehler: %s";
 $lang['danger']['spam_learn_error'] = "Spam Lernfehler: %s";
 $lang['success']['qlearn_spam'] = "Nachricht ID %s wurde als Spam gelernt und gelöscht";
 
@@ -645,9 +677,13 @@ $lang['mailbox']['recipient_map_new_info'] = 'Der neue Empfänger muss eine E-Ma
 $lang['mailbox']['recipient_map_old'] = 'Original Empfänger';
 $lang['mailbox']['recipient_map_new'] = 'Neuer Empfänger';
 $lang['mailbox']['add_recipient_map_entry'] = 'Empfängerumschreibung hinzufügen';
-$lang['danger']['invalid_recipient_map_new'] = 'Neuer Empfänger %s ist ungültig';
-$lang['danger']['invalid_recipient_map_old'] = 'Originaler Empfänger %s ist ungültig';
-$lang['danger']['recipient_map_entry_exists'] = 'Eine Empfängerumschreibung für %s existiert bereits';
-$lang['success']['recipient_map_entry_saved'] = 'Empfängerumschreibung für Objekt %s wurde gespeichert';
-$lang['success']['recipient_map_entry_deleted'] = 'Empfängerumschreibung für Objekt %s wurde gelöscht';
-
+$lang['danger']['invalid_recipient_map_new'] = 'Neuer Empfänger "%s" ist ungültig';
+$lang['danger']['invalid_recipient_map_old'] = 'Originaler Empfänger "%s" ist ungültig';
+$lang['danger']['recipient_map_entry_exists'] = 'Eine Empfängerumschreibung für Objekt "%s" existiert bereits';
+$lang['success']['recipient_map_entry_saved'] = 'Empfängerumschreibung für Objekt "%s" wurde gespeichert';
+$lang['success']['recipient_map_entry_deleted'] = 'Empfängerumschreibung mit der ID %s wurde gelöscht';
+$lang['danger']['tls_policy_map_entry_exists'] = 'Eine TLS-Richtlinie "%s" existiert bereits';
+$lang['success']['tls_policy_map_entry_saved'] = 'TLS-Richtlinieneintrag "%s" wurde gespeichert';
+$lang['success']['tls_policy_map_entry_deleted'] = 'TLS-Richtlinie mit der ID %s wurde gelöscht';
+$lang['mailbox']['add_tls_policy_map'] = "TLS-Richtlinieneintrag hinzufügen";
+$lang['danger']['tls_policy_map_parameter_invalid'] = "Parameter ist ungültig";

+ 41 - 4
data/web/lang/lang.en.php

@@ -16,6 +16,9 @@ $lang['footer']['delete_these_items'] = 'Please confirm your changes to the foll
 $lang['footer']['delete_now'] = 'Delete now';
 $lang['footer']['cancel'] = 'Cancel';
 
+$lang['footer']['hibp_nok'] = 'Matched! This is a potentially dangerous password!';
+$lang['footer']['hibp_ok'] = 'No match found.';
+
 $lang['danger']['mysql_error'] = "MySQL error: %s";
 $lang['danger']['redis_error'] = "Redis error: %s";
 $lang['danger']['unknown_tfa_method'] = "Unknown TFA method";
@@ -43,6 +46,7 @@ $lang['danger']['domain_cannot_match_hostname'] = "Domain cannot match hostname"
 $lang['warning']['domain_added_sogo_failed'] = "Added domain but failed to restart SOGo, please check your server logs.";
 $lang['danger']['rl_timeframe'] = "Rate limit time frame is incorrect";
 $lang['success']['rl_saved'] = "Rate limit for object %s saved";
+$lang['success']['acl_saved'] = "ACL for object %s saved";
 $lang['success']['deleted_syncjobs'] = "Deleted syncjobs: %s";
 $lang['success']['deleted_syncjob'] = "Deleted syncjob ID %s";
 $lang['success']['delete_filters'] = "Deleted filters: %s";
@@ -238,6 +242,17 @@ $lang['header']['mailcow_settings'] = 'Configuration';
 $lang['header']['administration'] = 'Administration';
 $lang['header']['mailboxes'] = 'Mailboxes';
 $lang['header']['user_settings'] = 'User settings';
+$lang['mailbox']['tls_policy_maps'] = 'TLS policy maps';
+$lang['mailbox']['tls_policy_maps_long'] = 'Outgoing TLS policy map overrides';
+$lang['mailbox']['tls_policy_maps_info'] = 'This policy map overrides outgoing TLS transport rules independently of a users TLS policy settings.<br>
+  Please check <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">the "smtp_tls_policy_maps" docs</a> for further information.';
+$lang['mailbox']['tls_enforce_in'] = 'Enforce TLS incoming';
+$lang['mailbox']['tls_enforce_out'] = 'Enforce TLS outgoing';
+$lang['mailbox']['tls_map_dest'] = 'Destination';
+$lang['mailbox']['tls_map_dest_info'] = 'Examples: example.org, .example.org, mail@example.org, [mail.example.org]:25';
+$lang['mailbox']['tls_map_policy'] = 'Policy';
+$lang['mailbox']['tls_map_parameters'] = 'Parameters';
+$lang['mailbox']['tls_map_parameters_info'] = 'Empty or parameters, for example: protocols=!SSLv2 ciphers=medium exclude=3DES';
 $lang['mailbox']['booking_0'] = 'Always show as free';
 $lang['mailbox']['booking_lt0'] = 'Unlimited, but show as busy when booked';
 $lang['mailbox']['booking_custom'] = 'Hard-limit to a custom amount of bookings';
@@ -342,6 +357,7 @@ $lang['edit']['full_name'] = 'Full name';
 $lang['edit']['quota_mb'] = 'Quota (MiB)';
 $lang['edit']['sender_acl'] = 'Allow to send as';
 $lang['edit']['sender_acl_disabled'] = '↳ <span class="label label-danger">Sender check is disabled</span>';
+$lang['user']['sender_acl_disabled'] = '<span class="label label-danger">Sender check is disabled</span>';
 $lang['edit']['previous'] = 'Previous page';
 $lang['edit']['unchanged_if_empty'] = 'If unchanged leave blank';
 $lang['edit']['dont_check_sender_acl'] = "Disable sender check for domain %s (+ alias domains)";
@@ -349,6 +365,22 @@ $lang['edit']['multiple_bookings'] = 'Multiple bookings';
 $lang['edit']['kind'] = 'Kind';
 $lang['edit']['resource'] = 'Resource';
 
+$lang['acl']['spam_alias'] = 'Temporary aliases';
+$lang['acl']['tls_policy'] = 'TLS policy';
+$lang['acl']['spam_score'] = 'Spam score';
+$lang['acl']['spam_policy'] = 'Blacklist/Whitelist';
+$lang['acl']['delimiter_action'] = 'Delimiter action';
+$lang['acl']['syncjobs'] = 'Sync jobs';
+$lang['acl']['eas_reset'] = 'Reset EAS devices';
+$lang['acl']['quarantine'] = 'Quarantine';
+$lang['acl']['login_as'] = 'Login as mailbox user';
+$lang['acl']['bcc_maps'] = 'BCC maps';
+$lang['acl']['filters'] = 'Filters';
+$lang['acl']['ratelimit'] = 'Rate limit';
+$lang['acl']['recipient_maps'] = 'Recipient maps';
+$lang['acl']['prohibited'] = 'Prohibited by ACL';
+
+$lang['add']['generate'] = 'generate';
 $lang['add']['syncjob'] = 'Add sync job';
 $lang['add']['syncjob_hint'] = 'Be aware that passwords need to be saved plain-text!';
 $lang['add']['hostname'] = 'Hostname';
@@ -610,7 +642,7 @@ $lang['quarantine']['subj'] = "Subject";
 $lang['quarantine']['text_plain_content'] = "Content (text/plain)";
 $lang['quarantine']['text_from_html_content'] = "Content (converted html)";
 $lang['quarantine']['atts'] = "Attachments";
-$lang['danger']['fuzzy_learn_error'] = "Fuzzy hash learn error: %s";
+$lang['warning']['fuzzy_learn_error'] = "Fuzzy hash learn error: %s";
 $lang['danger']['spam_learn_error'] = "Spam learn error: %s";
 $lang['success']['qlearn_spam'] = "Message ID %s was learned as spam and deleted";
 
@@ -646,6 +678,7 @@ $lang['mailbox']['bcc_maps'] = "BCC maps";
 $lang['mailbox']['bcc_to_sender'] = "Switch to sender map type";
 $lang['mailbox']['bcc_to_rcpt'] = "Switch to recipient map type";
 $lang['mailbox']['add_bcc_entry'] = "Add BCC map";
+$lang['mailbox']['add_tls_policy_map'] = "Add TLS policy map";
 $lang['mailbox']['bcc_info'] = "BCC maps are used to silently forward copies of all messages to another address. A recipient map type entry is used, when the local destination acts as recipient of a mail. Sender maps conform to the same principle.<br/>
   The local destination will not be informed about a failed delivery.";
 $lang['mailbox']['address_rewriting'] = 'Address rewriting';
@@ -658,10 +691,14 @@ $lang['mailbox']['recipient_map_old'] = 'Original recipient';
 $lang['mailbox']['recipient_map_new'] = 'New recipient';
 $lang['danger']['invalid_recipient_map_new'] = 'Invalid new recipient specified: %s';
 $lang['danger']['invalid_recipient_map_old'] = 'Invalid original recipient specified: %s';
-$lang['danger']['recipient_map_entry_exists'] = 'A Recipient map entry for %s exists';
-$lang['success']['recipient_map_entry_saved'] = 'Recipient map entry for %s has been saved';
-$lang['success']['recipient_map_entry_deleted'] = 'Recipient map entry for %s has been deleted';
+$lang['danger']['recipient_map_entry_exists'] = 'A Recipient map entry "%s" exists';
+$lang['success']['recipient_map_entry_saved'] = 'Recipient map entry "%s" has been saved';
+$lang['success']['recipient_map_entry_deleted'] = 'Recipient map ID %s has been deleted';
+$lang['danger']['tls_policy_map_entry_exists'] = 'A TLS policy map entry "%s" exists';
+$lang['success']['tls_policy_map_entry_saved'] = 'TLS policy map entry "%s" has been saved';
+$lang['success']['tls_policy_map_entry_deleted'] = 'TLS policy map ID %s has been deleted';
 $lang['mailbox']['add_recipient_map_entry'] = 'Add recipient map';
+$lang['danger']['tls_policy_map_parameter_invalid'] = "Policy parameter is invalid";
 
 $lang['oauth2']['scope_ask_permission'] = 'An application asked for the following permissions';
 $lang['oauth2']['profile'] = 'Profile';

+ 1 - 1
data/web/lang/lang.nl.php

@@ -592,7 +592,7 @@ $lang['quarantine']['subj'] = "Onderwerp";
 $lang['quarantine']['text_plain_content'] = "Inhoud (tekst)";
 $lang['quarantine']['text_from_html_content'] = "Inhoud (geconverteerde html)";
 $lang['quarantine']['atts'] = "Bijlagen";
-$lang['danger']['fuzzy_learn_error'] = "Fuzzy hash training-fout: %s";
+$lang['warning']['fuzzy_learn_error'] = "Fuzzy hash training-fout: %s";
 $lang['danger']['spam_learn_error'] = "Spamtraining-fout: %s";
 $lang['success']['qlearn_spam'] = "Bericht %s werd als spam geclassificeerd en is verwijderd";
 

+ 70 - 44
data/web/mailbox.php

@@ -22,6 +22,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
     <li role="presentation"><a href="#tab-syncjobs" aria-controls="tab-syncjobs" role="tab" data-toggle="tab"><?=$lang['mailbox']['sync_jobs'];?></a></li>
     <li role="presentation"><a href="#tab-filters" aria-controls="tab-filters" role="tab" data-toggle="tab"><?=$lang['mailbox']['filters'];?></a></li>
     <li role="presentation"><a href="#tab-bcc" aria-controls="tab-filters" role="tab" data-toggle="tab"><?=$lang['mailbox']['address_rewriting'];?></a></li>
+    <li role="presentation"><a href="#tab-tls-policy" aria-controls="tab-tls-policy" role="tab" data-toggle="tab"><?=$lang['mailbox']['tls_policy_maps'];?></a></li>
   </ul>
 
 	<div class="row">
@@ -44,10 +45,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
                   <? if($_SESSION['mailcow_cc_role'] == "admin"): ?>
-                    <li><a id="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                    <li><a id="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                    <li><a data-action="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                    <li><a data-action="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                     <li role="separator" class="divider"></li>
-                    <li><a id="delete_selected" data-id="domain" data-api-url='delete/domain' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                    <li><a data-action="delete_selected" data-id="domain" data-api-url='delete/domain' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                   <? endif; ?>
                 </ul>
                 <? if($_SESSION['mailcow_cc_role'] == "admin"): ?>
@@ -74,10 +75,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="mailbox" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="mailbox" data-api-url='delete/mailbox' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-id="mailbox" data-api-url='delete/mailbox' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addMailboxModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_mailbox'];?></a>
               </div>
@@ -101,10 +102,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="resource" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="resource" data-api-url='edit/resource' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="resource" data-api-url='edit/resource' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="resource" data-api-url='edit/resource' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="resource" data-api-url='edit/resource' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="resource" data-api-url='delete/resource' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-id="resource" data-api-url='delete/resource' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addResourceModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_resource'];?></a>
               </div>
@@ -134,10 +135,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="alias-domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="alias-domain" data-api-url='edit/alias-domain' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="alias-domain" data-api-url='edit/alias-domain' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="alias-domain" data-api-url='edit/alias-domain' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="alias-domain" data-api-url='edit/alias-domain' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="alias-domain" data-api-url='delete/alias-domain' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-id="alias-domain" data-api-url='delete/alias-domain' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addAliasDomainModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_domain_alias'];?></a>
               </div>
@@ -161,10 +162,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="alias" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="alias" data-api-url='delete/alias' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-id="alias" data-api-url='delete/alias' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addAliasModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_alias'];?></a>
               </div>
@@ -184,16 +185,16 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
               <table class="table table-striped" id="sync_job_table"></table>
             </div>
             <div class="mass-actions-mailbox">
-              <div class="btn-group">
+              <div class="btn-group" data-acl="<?=$_SESSION['acl']['syncjobs'];?>">
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="syncjob" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"last_run":""}' href="#"><?=$lang['mailbox']['last_run_reset'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"last_run":""}' href="#"><?=$lang['mailbox']['last_run_reset'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="syncjob" data-api-url='delete/syncjob' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="syncjob" data-api-url='delete/syncjob' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addSyncJobModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['create_syncjob'];?></a>
               </div>
@@ -214,17 +215,17 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
               <table class="table table-striped" id="filter_table"></table>
             </div>
             <div class="mass-actions-mailbox">
-              <div class="btn-group">
+              <div class="btn-group" data-acl="<?=$_SESSION['acl']['filters'];?>">
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="filter_item" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"prefilter"}' href="#"><?=$lang['mailbox']['set_prefilter'];?></a></li>
-                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#"><?=$lang['mailbox']['set_postfilter'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"prefilter"}' href="#"><?=$lang['mailbox']['set_prefilter'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#"><?=$lang['mailbox']['set_postfilter'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="filter_item" data-api-url='delete/filter' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="filter_item" data-api-url='delete/filter' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addFilterModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_filter'];?></a>
               </div>
@@ -245,23 +246,23 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
               <table class="table table-striped" id="bcc_table"></table>
             </div>
             <div class="mass-actions-mailbox">
-              <div class="btn-group">
+              <div class="btn-group" data-acl="<?=$_SESSION['acl']['bcc_maps'];?>">
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="bcc" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"type":"sender"}' href="#"><?=$lang['mailbox']['bcc_to_sender'];?></a></li>
-                  <li><a id="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"type":"rcpt"}' href="#"><?=$lang['mailbox']['bcc_to_rcpt'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"type":"sender"}' href="#"><?=$lang['mailbox']['bcc_to_sender'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"type":"rcpt"}' href="#"><?=$lang['mailbox']['bcc_to_rcpt'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="bcc" data-api-url='delete/bcc' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-id="bcc" data-api-url='delete/bcc' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addBCCModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_bcc_entry'];?></a>
               </div>
             </div>
           </div>
-          <div class="panel panel-default">
+          <div class="panel panel-default <?=($_SESSION['mailcow_cc_role'] == "admin") ?: 'hidden';?>">
             <div class="panel-heading">
               <?=$lang['mailbox']['recipient_maps'];?> <span class="badge badge-info table-lines"></span>
               <div class="btn-group pull-right">
@@ -272,27 +273,49 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
             <div class="table-responsive">
               <table class="table table-striped" id="recipient_map_table"></table>
             </div>
-<?php
-if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"))
-  $display = 'block';
-else
-  $display = 'none';
-?>
             <div class="mass-actions-mailbox" style="display: <?php echo $display; ?>">
               <div class="btn-group">
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="recipient_map" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="recipient_map" data-api-url='edit/recipient_map' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="recipient_map" data-api-url='edit/recipient_map' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="recipient_map" data-api-url='edit/recipient_map' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="recipient_map" data-api-url='edit/recipient_map' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="recipient_map" data-api-url='delete/recipient_map' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <li><a data-action="delete_selected" data-id="recipient_map" data-api-url='delete/recipient_map' href="#"><?=$lang['mailbox']['remove'];?></a></li>
                 </ul>
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addRecipientMapModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_recipient_map_entry'];?></a>
               </div>
             </div>
           </div>
         </div>
+
+        <div role="tabpanel" class="tab-pane <?=($_SESSION['mailcow_cc_role'] == "admin") ?: 'hidden';?>" id="tab-tls-policy">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <?=$lang['mailbox']['tls_policy_maps_long'];?> <span class="badge badge-info table-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default refresh_table" data-draw="draw_tls_policy_table" data-table="tls_policy_table"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <p style="margin:10px" class="help-block"><?=$lang['mailbox']['tls_policy_maps_info'];?></p>
+            <div class="table-responsive">
+              <table class="table table-striped" id="tls_policy_table"></table>
+            </div>
+            <div class="mass-actions-mailbox">
+              <div class="btn-group">
+                <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="tls-policy-map" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
+                <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
+                <ul class="dropdown-menu">
+                  <li><a data-action="edit_selected" data-id="tls-policy-map" data-api-url='edit/tls-policy-map' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a data-action="edit_selected" data-id="tls-policy-map" data-api-url='edit/tls-policy-map' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li role="separator" class="divider"></li>
+                  <li><a data-action="delete_selected" data-id="tls-policy-map" data-api-url='delete/tls-policy-map' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                </ul>
+                <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addTLSPolicyMapAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_tls_policy_map'];?></a>
+              </div>
+            </div>
+          </div>
+        </div>
       </div> <!-- /tab-content -->
     </div> <!-- /col-md-12 -->
   </div> <!-- /row -->
@@ -304,14 +327,17 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/mailbox.php';
 <?php
 $lang_mailbox = json_encode($lang['mailbox']);
 echo "var lang = ". $lang_mailbox . ";\n";
+echo "var acl = '". json_encode($_SESSION['acl']) . "';\n";
 echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
 $role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
+$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
 echo "var role = '". $role . "';\n";
+echo "var is_dual = " . $is_dual . ";\n";
 echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 ?>
 </script>
-<script src="js/footable.min.js"></script>
-<script src="js/mailbox.js"></script>
+<script src="/js/footable.min.js"></script>
+<script src="/js/mailbox.js"></script>
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
 } else {

+ 10 - 10
data/web/modals/admin.php

@@ -17,13 +17,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="desc"><?=$lang['admin']['rsetting_desc'];?>:</label>
             <div class="col-sm-10">
-              <input type="text" class="form-control" name="desc" id="desc" required>
+              <input type="text" class="form-control" id="adminRspamdSettingsDesc" name="desc" required>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-2" for="content"><?=$lang['admin']['rsetting_content'];?>:</label>
             <div class="col-sm-10">
-              <textarea class="form-control" id="content" name="content" rows="10"><?=$rsetting_details['content'];?></textarea>
+              <textarea class="form-control" id="adminRspamdSettingsContent" name="content" rows="10"><?=$rsetting_details['content'];?></textarea>
             </div>
           </div>
           <div class="form-group">
@@ -35,7 +35,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           </div>
           <div class="form-group">
             <div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="rsetting" data-api-url='add/rsetting' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="rsetting" data-api-url='add/rsetting' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <?=$lang['admin']['add'];?></button>
             </div>
           </div>
         </form>
@@ -60,14 +60,14 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="username"><?=$lang['admin']['username'];?>:</label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="username" id="username" required>
+                <input type="text" class="form-control" name="username" required>
                 &rdsh; <kbd>a-z A-Z - _ .</kbd>
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="name"><?=$lang['admin']['admin_domains'];?>:</label>
               <div class="col-sm-10">
-                <select title="<?=$lang['admin']['search_domain_da'];?>" style="width:100%" name="domains" size="5" multiple>
+                <select title="<?=$lang['admin']['search_domain_da'];?>" class="full-width-select" name="domains" size="5" multiple>
                 <?php
                 foreach (mailbox('get', 'domains') as $domain) {
                   echo "<option>".htmlspecialchars($domain)."</option>";
@@ -79,13 +79,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
               <label class="control-label col-sm-2" for="password"><?=$lang['admin']['password'];?>:</label>
               <div class="col-sm-10">
-              <input type="password" class="form-control" name="password" id="password" placeholder="" required>
+              <input type="password" class="form-control" data-hibp="true" name="password" placeholder="" required>
               </div>
             </div>
             <div class="form-group">
               <label class="control-label col-sm-2" for="password2"><?=$lang['admin']['password_repeat'];?>:</label>
               <div class="col-sm-10">
-              <input type="password" class="form-control" name="password2" id="password2" placeholder="" required>
+              <input type="password" class="form-control" name="password2" placeholder="" required>
               </div>
             </div>
             <div class="form-group">
@@ -97,7 +97,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             </div>
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
-                <button class="btn btn-default" id="add_item" data-id="add_domain_admin" data-api-url='add/domain-admin' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <?=$lang['admin']['add'];?></button>
+                <button class="btn btn-default" data-action="add_item" data-id="add_domain_admin" data-api-url='add/domain-admin' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <?=$lang['admin']['add'];?></button>
               </div>
             </div>
           </form>
@@ -115,11 +115,11 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
       </div>
       <div class="modal-body">
           <form class="form-horizontal" data-cached-form="true" id="test_relayhost_form" role="form" method="post">
-            <input type="hidden" class="form-control" name="relayhost_id" id="relayhost_id">
+            <input type="hidden" class="form-control" name="relayhost_id">
             <div class="form-group">
               <label class="control-label col-sm-2" for="mail_from"><?=$lang['admin']['relay_from'];?></label>
               <div class="col-sm-10">
-                <input type="text" class="form-control" name="mail_from" id="mail_from" placeholder="relay@example.org">
+                <input type="text" class="form-control" name="mail_from" placeholder="relay@example.org">
               </div>
             </div>
             <div class="form-group">

+ 13 - 13
data/web/modals/footer.php

@@ -8,19 +8,19 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
       <div class="modal-body">
       <form role="form" method="post">
         <div class="form-group">
-          <input type="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
+          <input type="text" class="form-control" name="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
         </div>
         <hr>
         <p class="help-block"><?=$lang['tfa']['api_register'];?></p>
         <div class="form-group">
-          <input type="text" class="form-control" name="yubico_id" id="yubico_id" placeholder="Yubico API ID" autocomplete="off" required>
+          <input type="text" class="form-control" name="yubico_id" placeholder="Yubico API ID" autocomplete="off" required>
         </div>
         <div class="form-group">
-          <input type="text" class="form-control" name="yubico_key" id="yubico_key" placeholder="Yubico API Key" autocomplete="off" required>
+          <input type="text" class="form-control" name="yubico_key" placeholder="Yubico API Key" autocomplete="off" required>
         </div>
         <hr>
         <div class="form-group">
-          <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+          <input type="password" class="form-control" name="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
         </div>
         <div class="form-group">
           <div class="input-group">
@@ -43,10 +43,10 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
       <div class="modal-body">
         <form role="form" method="post" id="u2f_reg_form">
           <div class="form-group">
-            <input type="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
+            <input type="text" class="form-control" name="key_id" placeholder="<?=$lang['tfa']['key_id'];?>" autocomplete="off" required>
           </div>
           <div class="form-group">
-            <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+            <input type="password" class="form-control" name="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
           </div>
           <hr>
           <p id="u2f_status_reg"></p>
@@ -67,16 +67,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
       <div class="modal-body">
         <form role="form" method="post">
           <div class="form-group">
-            <input type="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id_totp'];?>" autocomplete="off" required>
+            <input type="text" class="form-control" name="key_id" placeholder="<?=$lang['tfa']['key_id_totp'];?>" autocomplete="off" required>
           </div>
           <div class="form-group">
-            <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+            <input type="password" class="form-control" name="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
           </div>
           <hr>
           <?php
           $totp_secret = $tfa->createSecret();
           ?>
-          <input type="hidden" value="<?=$totp_secret;?>" name="totp_secret" id="totp_secret"/>
+          <input type="hidden" value="<?=$totp_secret;?>" name="totp_secret">
           <input type="hidden" name="tfa_method" value="totp">
           <ol>
             <li>
@@ -88,7 +88,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
             </li>
             <li>
               <p><?=$lang['tfa']['confirm_totp_token'];?>:</p>
-              <p><input type="number" style="width:33%" class="form-control" name="totp_confirm_token" id="totp_confirm_token" autocomplete="off" required></p>
+              <p><input type="number" style="width:33%" class="form-control" name="totp_confirm_token" autocomplete="off" required></p>
               <p><button class="btn btn-default" type="submit" name="set_tfa"><?=$lang['tfa']['confirm'];?></button></p>
             </li>
           </ol>
@@ -105,7 +105,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
       <div class="modal-body">
         <form role="form" method="post">
           <div class="input-group">
-            <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
+            <input type="password" class="form-control" name="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
             <span class="input-group-btn">
               <input type="hidden" name="tfa_method" value="none">
               <button class="btn btn-danger" type="submit" name="set_tfa"><?=$lang['tfa']['delete_tfa'];?></button>
@@ -135,7 +135,7 @@ if (isset($_SESSION['pending_tfa_method'])):
           <div class="form-group">
             <div class="input-group">
               <span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
-              <input type="text" name="token" id="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
+              <input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
               <input type="hidden" name="tfa_method" value="yubi_otp">
             </div>
           </div>
@@ -160,7 +160,7 @@ if (isset($_SESSION['pending_tfa_method'])):
           <div class="form-group">
             <div class="input-group">
               <span class="input-group-addon" id="tfa-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></span>
-              <input type="number" min="000000" max="999999" name="token" id="token" class="form-control" placeholder="123456" aria-describedby="tfa-addon">
+              <input type="number" min="000000" max="999999" name="token" class="form-control" placeholder="123456" aria-describedby="tfa-addon">
               <input type="hidden" name="tfa_method" value="totp">
             </div>
           </div>

+ 108 - 53
data/web/modals/mailbox.php

@@ -17,13 +17,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="local_part"><?=$lang['add']['mailbox_username'];?></label>
             <div class="col-sm-10">
-              <input type="text" pattern="[A-Za-z0-9\.!#$%&'*+/=?^_`{|}~-]+" autocorrect="off" autocapitalize="none" class="form-control" name="local_part" id="local_part" required>
+              <input type="text" pattern="[A-Za-z0-9\.!#$%&'*+/=?^_`{|}~-]+" autocorrect="off" autocapitalize="none" class="form-control" name="local_part" required>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
             <div class="col-sm-10">
-              <select data-live-search="true" id="addSelectDomain" name="domain" id="domain" required>
+              <select class="full-width-select" data-live-search="true" id="addSelectDomain" name="domain" required>
               <?php
               foreach (mailbox('get', 'domains') as $domain) {
                 echo "<option>".htmlspecialchars($domain)."</option>";
@@ -35,7 +35,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="name"><?=$lang['add']['full_name'];?></label>
             <div class="col-sm-10">
-            <input type="text" class="form-control" name="name" id="name">
+            <input type="text" class="form-control" name="name">
             </div>
           </div>
           <div class="form-group">
@@ -48,16 +48,15 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             </div>
           </div>
           <div class="form-group">
-            <label class="control-label col-sm-2" for="password"><?=$lang['add']['password'];?></label>
+            <label class="control-label col-sm-2" for="password"><?=$lang['add']['password'];?> (<a href="#" class="generate_password"><?=$lang['add']['generate'];?></a>)</label>
             <div class="col-sm-10">
-            <input type="password" class="form-control" name="password" id="password" placeholder="" required>
-            (<a href="#" class="generate_password">Generate</a>)
+            <input type="password" data-hibp="true" class="form-control" name="password" placeholder="" required>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-2" for="password2"><?=$lang['add']['password_repeat'];?></label>
             <div class="col-sm-10">
-            <input type="password" class="form-control" name="password2" id="password2" placeholder="" required>
+            <input type="password" class="form-control" name="password2" placeholder="" required>
             </div>
           </div>
           <div class="form-group">
@@ -69,7 +68,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           </div>
           <div class="form-group">
             <div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_mailbox" data-api-url='add/mailbox' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_mailbox" data-api-url='add/mailbox' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
             </div>
           </div>
         </form>
@@ -90,37 +89,37 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
 						<div class="col-sm-10">
-						<input type="text" autocorrect="off" autocapitalize="none" class="form-control" name="domain" id="domain" required>
+						<input type="text" autocorrect="off" autocapitalize="none" class="form-control" name="domain" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="description" id="description" required>
+						<input type="text" class="form-control" name="description" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="aliases"><?=$lang['add']['max_aliases'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="aliases" id="aliases" value="400" required>
+						<input type="number" class="form-control" name="aliases" value="400" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="mailboxes"><?=$lang['add']['max_mailboxes'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="mailboxes" id="mailboxes" value="10" required>
+						<input type="number" class="form-control" name="mailboxes" value="10" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="maxquota"><?=$lang['add']['mailbox_quota_m'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="maxquota" id="maxquota" value="3072" required>
+						<input type="number" class="form-control" name="maxquota" value="3072" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="quota"><?=$lang['add']['domain_quota_m'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="quota" id="quota" value="10240" required>
+						<input type="number" class="form-control" name="quota" value="10240" required>
 						</div>
 					</div>
 					<div class="form-group">
@@ -134,10 +133,10 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="quota">Ratelimit</label>
             <div class="col-sm-7">
-            <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
+            <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
             </div>
             <div class="col-sm-3">
-            <select name="rl_frame" id="rl_frame" class="form-control">
+            <select name="rl_frame" class="form-control">
               <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
               <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
               <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
@@ -159,8 +158,8 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <hr>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{}' href="#"><?=$lang['add']['add_domain_only'];?></button>
-              <button class="btn btn-default" id="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"restart_sogo":"1"}' href="#"><?=$lang['add']['add_domain_restart'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{}' href="#"><?=$lang['add']['add_domain_only'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"restart_sogo":"1"}' href="#"><?=$lang['add']['add_domain_restart'];?></button>
 						</div>
 					</div>
 					<p><span class="glyphicon glyphicon-exclamation-sign text-danger"></span> <?=$lang['add']['restart_sogo_hint'];?></p>
@@ -182,13 +181,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
 						<div class="col-sm-10">
-							<input type="text" class="form-control" name="description" id="description" required>
+							<input type="text" class="form-control" name="description" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
 						<div class="col-sm-10">
-							<select data-live-search="true" name="domain" id="domain" title="<?=$lang['add']['select'];?>" required>
+							<select data-live-search="true" name="domain" title="<?=$lang['add']['select'];?>" required>
 							<?php
               foreach (mailbox('get', 'domains') as $domain) {
 								echo "<option>".htmlspecialchars($domain)."</option>";
@@ -200,7 +199,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['kind'];?>:</label>
 						<div class="col-sm-10">
-							<select name="kind" id="kind" title="<?=$lang['add']['select'];?>" required>
+							<select name="kind" title="<?=$lang['add']['select'];?>" required>
 								<option value="location">Location</option>
 								<option value="group">Group</option>
 								<option value="thing">Thing</option>
@@ -231,7 +230,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_resource" data-api-url='add/resource' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_resource" data-api-url='add/resource' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -263,13 +262,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 							<textarea id="textarea_alias_goto" autocorrect="off" autocapitalize="none" class="form-control" rows="5" id="goto" name="goto" required></textarea>
 							<p><?=$lang['add']['target_address_info'];?></p>
 							<div class="checkbox">
-                <label><input class="goto_checkbox" id="goto_null" type="checkbox" value="1" name="goto_null"> <?=$lang['add']['goto_null'];?></label>
+                <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_null"> <?=$lang['add']['goto_null'];?></label>
 							</div>
               <div class="checkbox">
-                <label><input class="goto_checkbox" id="goto_spam" type="checkbox" value="1" name="goto_spam"> <?=$lang['add']['goto_spam'];?></label>
+                <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_spam"> <?=$lang['add']['goto_spam'];?></label>
 							</div>
               <div class="checkbox">
-                <label><input class="goto_checkbox" id="goto_ham" type="checkbox" value="1" name="goto_ham"> <?=$lang['add']['goto_ham'];?></label>
+                <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_ham"> <?=$lang['add']['goto_ham'];?></label>
 							</div>
 						</div>
 					</div>
@@ -282,7 +281,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_alias" data-api-url='add/alias' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_alias" data-api-url='add/alias' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -311,7 +310,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="target_domain"><?=$lang['add']['target_domain'];?></label>
 						<div class="col-sm-10">
-							<select data-live-search="true" name="target_domain" id="target_domain" title="<?=$lang['add']['select'];?>" required>
+							<select data-live-search="true" name="target_domain" title="<?=$lang['add']['select'];?>" required>
 							<?php
               foreach (mailbox('get', 'domains') as $domain) {
 								echo "<option>".htmlspecialchars($domain)."</option>";
@@ -329,7 +328,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_alias_domain" data-api-url='add/alias-domain' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_alias_domain" data-api-url='add/alias-domain' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -351,7 +350,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
             <div class="col-sm-10">
-              <select data-live-search="true" id="addSelectUsername" name="username" id="username" required>
+              <select data-live-search="true" name="username" required>
               <?php
               $domains = mailbox('get', 'domains');
               if (!empty($domains)) {
@@ -369,32 +368,32 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="host1"><?=$lang['add']['hostname'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="host1" id="host1" required>
+						<input type="text" class="form-control" name="host1" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="port1"><?=$lang['add']['port'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="143" required>
+						<input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required>
             <small class="help-block">1-65535</small>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="user1"><?=$lang['add']['username'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="user1" id="user1" required>
+						<input type="text" class="form-control" name="user1" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="password1"><?=$lang['add']['password'];?></label>
 						<div class="col-sm-10">
-						<input type="password" class="form-control" name="password1" id="password1" required>
+						<input type="password" class="form-control" name="password1" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="enc1"><?=$lang['add']['enc_method'];?></label>
 						<div class="col-sm-10">
-							<select name="enc1" id="enc1" title="<?=$lang['add']['select'];?>" required>
+							<select name="enc1" title="<?=$lang['add']['select'];?>" required>
                 <option selected>TLS</option>
                 <option>SSL</option>
                 <option>PLAIN</option>
@@ -411,47 +410,47 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="subfolder2"><?=$lang['edit']['subfolder2'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="subfolder2" id="subfolder2" value="External">
+						<input type="text" class="form-control" name="subfolder2" value="External">
 						</div>
 					</div>
 					<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" min="0" max="32000" value="0">
+						<input type="number" class="form-control" name="maxage" min="0" max="32000" value="0">
             <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">
+						<input type="number" class="form-control" name="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="timeout1"><?=$lang['edit']['timeout1'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="timeout1" id="timeout1" min="1" max="32000" value="600">
+						<input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600">
             <small class="help-block">1-32000</small>
 						</div>
 					</div>
           <div class="form-group">
 						<label class="control-label col-sm-2" for="timeout2"><?=$lang['edit']['timeout2'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="timeout2" id="timeout2" min="1" max="32000" value="600">
+						<input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600">
             <small class="help-block">1-32000</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">
-						<input type="text" class="form-control" name="exclude" id="exclude" value="(?i)spam|(?i)junk">
+						<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="custom_params" id="custom_params" placeholder="--delete2folders --otheroption">
+						<input type="text" class="form-control" name="custom_params" placeholder="--delete2folders --otheroption">
 						</div>
 					</div>
 					<div class="form-group">
@@ -505,7 +504,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_syncjob" data-api-url='add/syncjob' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_syncjob" data-api-url='add/syncjob' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -526,7 +525,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
             <div class="col-sm-10">
-              <select data-live-search="true" id="addSelectUsername" name="username" id="username" required>
+              <select data-live-search="true" name="username" required>
               <?php
               $domains = mailbox('get', 'domains');
               if (!empty($domains)) {
@@ -544,7 +543,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="filter_type"><?=$lang['add']['sieve_type'];?>:</label>
             <div class="col-sm-10">
-              <select id="addFilterType" name="filter_type" id="filter_type" required>
+              <select id="addFilterType" name="filter_type" required>
                 <option value="prefilter">Prefilter</option>
                 <option value="postfilter">Postfilter</option>
               </select>
@@ -553,7 +552,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="script_desc"><?=$lang['add']['sieve_desc'];?>:</label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="script_desc" id="script_desc" required maxlength="255">
+						<input type="text" class="form-control" name="script_desc" required maxlength="255">
 						</div>
 					</div>
 					<div class="form-group">
@@ -573,7 +572,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10" id="add_filter_btns">
               <button class="btn btn-default" id="validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
-              <button class="btn btn-success" id="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-success" data-action="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -594,7 +593,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="local_dest"><?=$lang['mailbox']['bcc_local_dest'];?>:</label>
             <div class="col-sm-10">
-              <select data-live-search="true" id="addSelectLocalDest" name="local_dest" id="local_dest" required>
+              <select data-live-search="true" name="local_dest" required>
               <?php
               $domains = mailbox('get', 'domains');
               $alias_domains = mailbox('get', 'alias_domains');
@@ -623,7 +622,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="type"><?=$lang['mailbox']['bcc_map_type'];?>:</label>
             <div class="col-sm-10">
-              <select id="addFBCCType" name="type" id="type" required>
+              <select name="type" required>
                 <option value="sender"><?=$lang['mailbox']['bcc_sender_map'];?></option>
                 <option value="rcpt"><?=$lang['mailbox']['bcc_rcpt_map'];?></option>
               </select>
@@ -632,7 +631,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="bcc_dest"><?=$lang['mailbox']['bcc_destination'];?>:</label>
 						<div class="col-sm-10">
-              <input type="text" class="form-control" name="bcc_dest" id="bcc_dest">
+              <input type="text" class="form-control" name="bcc_dest">
 						</div>
 					</div>
 					<div class="form-group">
@@ -644,7 +643,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-success" id="add_item" data-id="add_bcc" data-api-url='add/bcc' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-success" data-action="add_item" data-id="add_bcc" data-api-url='add/bcc' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -665,14 +664,14 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="recipient_map_old"><?=$lang['mailbox']['recipient_map_old'];?></label>
             <div class="col-sm-10">
-            <input type="text" class="form-control" name="recipient_map_old" id="recipient_map_old">
+            <input type="text" class="form-control" name="recipient_map_old">
             <small><?=$lang['mailbox']['recipient_map_old_info'];?></small>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-2" for="recipient_map_new"><?=$lang['mailbox']['recipient_map_new'];?></label>
             <div class="col-sm-10">
-            <input type="text" class="form-control" name="recipient_map_new" id="recipient_map_new">
+            <input type="text" class="form-control" name="recipient_map_new">
             <small><?=$lang['mailbox']['recipient_map_new_info'];?></small>
             </div>
           </div>
@@ -685,7 +684,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-success" id="add_item" data-id="add_recipient_map" data-api-url='add/recipient_map' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-success" data-action="add_item" data-id="add_recipient_map" data-api-url='add/recipient_map' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -693,6 +692,62 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
   </div>
 </div><!-- add add_recipient_map modal -->
+<!-- add add_tls_policy_map modal -->
+<div class="modal fade" id="addTLSPolicyMapAdmin" tabindex="-1" role="dialog" aria-hidden="true">
+  <div class="modal-dialog modal-lg">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
+        <h3 class="modal-title"><?=$lang['mailbox']['tls_policy_maps'];?></h3>
+      </div>
+      <div class="modal-body">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_tls_policy_map">
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="dest"><?=$lang['mailbox']['tls_map_dest'];?></label>
+            <div class="col-sm-10">
+            <input type="text" class="form-control" name="dest">
+            <small><?=$lang['mailbox']['tls_map_dest_info'];?></small>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="policy"><?=$lang['mailbox']['tls_map_policy'];?>:</label>
+            <div class="col-sm-10">
+              <select class="full-width-select" name="policy" required>
+                <option value="none">none</option>
+                <option value="may">may</option>
+                <option value="encrypt">encrypt</option>
+                <option value="dane">dane</option>
+                <option value="dane-only">dane-only</option>
+                <option value="fingerprint">fingerprint</option>
+                <option value="verify">verify</option>
+                <option value="secure">secure</option>
+              </select>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="parameters"><?=$lang['mailbox']['tls_map_parameters'];?></label>
+            <div class="col-sm-10">
+            <input type="text" class="form-control" name="parameters">
+            <small><?=$lang['mailbox']['tls_map_parameters_info'];?></small>
+            </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="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+              <button class="btn btn-success" data-action="add_item" data-id="add_tls_policy_map" data-api-url='add/tls-policy-map' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+						</div>
+					</div>
+				</form>
+      </div>
+    </div>
+  </div>
+</div><!-- add add_tls_policy_map modal -->
 <!-- log modal -->
 <div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
   <div class="modal-dialog modal-lg" role="document">

+ 15 - 15
data/web/modals/user.php

@@ -18,32 +18,32 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="host1"><?=$lang['add']['hostname'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="host1" id="host1" required>
+						<input type="text" class="form-control" name="host1" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="port1"><?=$lang['add']['port'];?></label>
 						<div class="col-sm-10">
-						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="143" required>
+						<input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required>
             <small class="help-block">1-65535</small>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="user1"><?=$lang['add']['username'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="user1" id="user1" required>
+						<input type="text" class="form-control" name="user1" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="password1"><?=$lang['add']['password'];?></label>
 						<div class="col-sm-10">
-						<input type="password" class="form-control" name="password1" id="password1" required>
+						<input type="password" class="form-control" name="password1" data-hibp="true" required>
 						</div>
 					</div>
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="enc1"><?=$lang['add']['enc_method'];?></label>
 						<div class="col-sm-10">
-							<select name="enc1" id="enc1" title="<?=$lang['add']['select'];?>" required>
+							<select name="enc1" title="<?=$lang['add']['select'];?>" required>
                 <option selected>TLS</option>
                 <option>SSL</option>
                 <option>PLAIN</option>
@@ -60,27 +60,27 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="subfolder2"><?=$lang['edit']['subfolder2'];?></label>
 						<div class="col-sm-10">
-						<input type="text" class="form-control" name="subfolder2" id="subfolder2" value="External">
+						<input type="text" class="form-control" name="subfolder2" value="External">
 						</div>
 					</div>
 					<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" min="0" max="32000" value="0">
+						<input type="number" class="form-control" name="maxage" min="0" max="32000" value="0">
             <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">
+						<input type="number" class="form-control" name="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">
-						<input type="text" class="form-control" name="exclude" id="exclude" value="(?i)spam|(?i)junk">
+						<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
 						</div>
 					</div>
 					<div class="form-group">
@@ -127,7 +127,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
-              <button class="btn btn-default" id="add_item" data-id="add_syncjob" data-api-url='add/syncjob' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_syncjob" data-api-url='add/syncjob' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
 						</div>
 					</div>
 				</form>
@@ -155,13 +155,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
             <div class="col-sm-5">
-            <input type="password" class="form-control" name="user_new_pass" id="user_new_pass" autocomplete="off" required>
+            <input type="password" data-hibp="true" class="form-control" name="user_new_pass" autocomplete="off" required>
             </div>
           </div>
           <div class="form-group">
             <label class="control-label col-sm-3" for="user_new_pass2"><?=$lang['user']['new_password_repeat'];?></label>
             <div class="col-sm-5">
-            <input type="password" class="form-control" name="user_new_pass2" id="user_new_pass2" autocomplete="off" required>
+            <input type="password" class="form-control" name="user_new_pass2" autocomplete="off" required>
             <p class="help-block"><?=$lang['user']['new_password_description'];?></p>
             </div>
           </div>
@@ -169,12 +169,12 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-3" for="user_old_pass"><?=$lang['user']['password_now'];?></label>
             <div class="col-sm-5">
-            <input type="password" class="form-control" name="user_old_pass" id="user_old_pass" autocomplete="off" required>
+            <input type="password" class="form-control" name="user_old_pass" autocomplete="off" required>
             </div>
           </div>
           <div class="form-group">
             <div class="col-sm-offset-3 col-sm-9">
-              <button class="btn btn-default" id="edit_selected" data-id="pwchange" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#"><?=$lang['user']['change_password'];?></button>
+              <button class="btn btn-default" data-action="edit_selected" data-id="pwchange" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#"><?=$lang['user']['change_password'];?></button>
             </div>
           </div>
         </form>
@@ -195,4 +195,4 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
       </div>
     </div>
   </div>
-</div><!-- sieve filter modal -->
+</div><!-- sieve filter modal -->

+ 6 - 6
data/web/quarantine.php

@@ -18,15 +18,15 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
           <table id="quarantinetable" class="table table-striped"></table>
         </div>
         <div class="mass-actions-quarantine">
-          <div class="btn-group">
+          <div class="btn-group" data-acl="<?=$_SESSION['acl']['quarantine'];?>">
             <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="qitems" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['quarantine']['toggle_all'];?></a>
             <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['quarantine']['quick_actions'];?> <span class="caret"></span></a>
             <ul class="dropdown-menu">
-              <li><a id="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"release"}' href="#"><?=$lang['quarantine']['release'];?></a></li>
+              <li><a data-action="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"release"}' href="#"><?=$lang['quarantine']['release'];?></a></li>
               <li role="separator" class="divider"></li>
-              <li><a id="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"learnspam"}' href="#"><?=$lang['quarantine']['learn_spam_delete'];?></a></li>
+              <li><a data-action="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"learnspam"}' href="#"><?=$lang['quarantine']['learn_spam_delete'];?></a></li>
               <li role="separator" class="divider"></li>
-              <li><a id="delete_selected" data-id="qitems" data-api-url='delete/qitem' href="#"><?=$lang['quarantine']['remove'];?></a></li>
+              <li><a data-action="delete_selected" data-id="qitems" data-api-url='delete/qitem' href="#"><?=$lang['quarantine']['remove'];?></a></li>
             </ul>
           </div>
         </div>
@@ -47,8 +47,8 @@ echo "var role = '". $role . "';\n";
 echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 ?>
 </script>
-<script src="js/footable.min.js"></script>
-<script src="js/quarantine.js"></script>
+<script src="/js/footable.min.js"></script>
+<script src="/js/quarantine.js"></script>
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
 } else {

+ 50 - 93
data/web/user.php

@@ -10,6 +10,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
 	$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
   $tfa_data = get_tfa();
 	$username = $_SESSION['mailcow_cc_username'];
+
 ?>
 <div class="container">
   <h3><?=$lang['user']['user_settings'];?></h3>
@@ -28,6 +29,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
         else: echo "Last login: -"; endif;
         ?>
         </small></p>
+        <p>
       </div>
     </div>
     <hr>
@@ -88,7 +90,6 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
       fclose($fh);
     }
   }
-
 ?>
 <div class="container">
 <h3><?=$lang['user']['user_settings'];?></h3>
@@ -103,6 +104,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
       <?php endif; ?>
       <p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
       <p><a target="_blank" href="https://mailcow.github.io/mailcow-dockerized-docs/client/#<?=$clientconfigstr;?>">[<?=$lang['user']['client_configuration'];?>]</a></p>
+      <p><a href="#userFilterModal" data-toggle="modal">[<?=$lang['user']['show_sieve_filters'];?>]</a></p>
       <p><small>
       <?php
       if ($_SESSION['mailcow_cc_last_login']['remote']):
@@ -115,13 +117,6 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
     </div>
   </div>
   <hr>
-  <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">[<?=$lang['user']['show_sieve_filters'];?>]</a></p>
-    </div>
-  </div>
-  <hr>
   <?php // Get user information about aliases
   $user_get_alias_details = user_get_alias_details($username);
   ?>
@@ -145,7 +140,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   <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">
-    <p><?=$user_get_alias_details['aliases_also_send_as'];?></p>
+    <p><?=($user_get_alias_details['aliases_also_send_as'] == '*') ? $lang['user']['sender_acl_disabled'] : $user_get_alias_details['aliases_also_send_as'];?></p>
     </div>
   </div>
   <div class="row">
@@ -172,87 +167,71 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
       <p><?=formatBytes($mailboxdata['quota_used'], 2);?> / <?=formatBytes($mailboxdata['quota'], 2);?>, <?=$mailboxdata['messages'];?> <?=$lang['user']['messages'];?></p>
     </div>
   </div>
+  <hr>
   <?php
-  ($_SESSION['acl']['delimiter_action'] == 0 && $_SESSION['acl']['delimiter_action'] == 0 && $_SESSION['acl']['delimiter_action'] == 0) ? null : '<hr>';
   // Show tagging options
-  if ($_SESSION['acl']['delimiter_action'] == 1):
   $get_tagging_options = mailbox('get', 'delimiter_action', $username);
   ?>
   <div class="row">
     <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['tag_handling'];?>:</div>
     <div class="col-md-9 col-xs-7">
-    <div class="btn-group">
-
+    <div class="btn-group" data-acl="<?=$_SESSION['acl']['delimiter_action'];?>">
       <button type="button" class="btn btn-sm btn-default <?=($get_tagging_options == "subfolder") ? 'active' : null; ?>"
-        id="edit_selected"
+        data-action="edit_selected"
         data-item="<?= htmlentities($username); ?>"
         data-id="delimiter_action"
         data-api-url='edit/delimiter_action'
         data-api-attr='{"tagged_mail_handler":"subfolder"}'><?=$lang['user']['tag_in_subfolder'];?></button>
-
       <button type="button" class="btn btn-sm btn-default <?=($get_tagging_options == "subject") ? 'active' : null; ?>"
-        id="edit_selected"
+        data-action="edit_selected"
         data-item="<?= htmlentities($username); ?>"
         data-id="delimiter_action"
         data-api-url='edit/delimiter_action'
         data-api-attr='{"tagged_mail_handler":"subject"}'><?=$lang['user']['tag_in_subject'];?></button>
-
       <button type="button" class="btn btn-sm btn-default <?=($get_tagging_options == "none") ? 'active' : null; ?>"
-        id="edit_selected"
+        data-action="edit_selected"
         data-item="<?= htmlentities($username); ?>"
         data-id="delimiter_action"
         data-api-url='edit/delimiter_action'
         data-api-attr='{"tagged_mail_handler":"none"}'><?=$lang['user']['tag_in_none'];?></button>
-
     </div>
     <p class="help-block"><?=$lang['user']['tag_help_explain'];?></p>
     <p class="help-block"><?=$lang['user']['tag_help_example'];?></p>
     </div>
   </div>
   <?php
-  endif;
   // Show TLS policy options
-  if ($_SESSION['acl']['tls_policy'] == 1):
   $get_tls_policy = mailbox('get', 'tls_policy', $username);
   ?>
   <div class="row">
     <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['tls_policy'];?>:</div>
     <div class="col-md-9 col-xs-7">
-    <div class="btn-group">
-
+    <div class="btn-group" data-acl="<?=$_SESSION['acl']['tls_policy'];?>">
       <button type="button" class="btn btn-sm btn-default <?=($get_tls_policy['tls_enforce_in'] == "1") ? "active" : null;?>"
-        id="edit_selected"
+        data-action="edit_selected"
         data-item="<?= htmlentities($username); ?>"
         data-id="tls_policy"
         data-api-url='edit/tls_policy'
         data-api-attr='{"tls_enforce_in":<?=($get_tls_policy['tls_enforce_in'] == "1") ? "0" : "1";?>}'><?=$lang['user']['tls_enforce_in'];?></button>
-
       <button type="button" class="btn btn-sm btn-default <?=($get_tls_policy['tls_enforce_out'] == "1") ? "active" : null;?>"
-        id="edit_selected"
+        data-action="edit_selected"
         data-item="<?= htmlentities($username); ?>"
         data-id="tls_policy"
         data-api-url='edit/tls_policy'
         data-api-attr='{"tls_enforce_out":<?=($get_tls_policy['tls_enforce_out'] == "1") ? "0" : "1";?>}'><?=$lang['user']['tls_enforce_out'];?></button>
-
     </div>
     <p class="help-block"><?=$lang['user']['tls_policy_warning'];?></p>
     </div>
   </div>
-  <?php
-  endif;
-  // Rest EAS devices
-  if ($_SESSION['acl']['eas_reset'] == 1):
-  ?>
+
   <div class="row">
     <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['eas_reset'];?>:</div>
     <div class="col-md-9 col-xs-7">
-    <button class="btn btn-xs btn-default" id="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-item="<?= htmlentities($username); ?>" data-id="eas_cache" data-api-url='delete/eas_cache' href="#"><?=$lang['user']['eas_reset_now'];?></button>
+    <button class="btn btn-xs btn-default" data-acl="<?=$_SESSION['acl']['eas_reset'];?>" data-action="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-item="<?= htmlentities($username); ?>" data-id="eas_cache" data-api-url='delete/eas_cache' href="#"><?=$lang['user']['eas_reset_now'];?></button>
     <p class="help-block"><?=$lang['user']['eas_reset_help'];?></p>
     </div>
   </div>
-  <?php
-  endif;
-  ?>
+
 </div>
 </div>
 
@@ -273,35 +252,31 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
         </div>
       </div>
 		</div>
-    <?php
-    if ($_SESSION['acl']['spam_alias'] == 1):
-    ?>
+
     <div class="mass-actions-user">
-      <div class="btn-group">
+      <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_alias'];?>">
         <div class="btn-group">
           <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="tla" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
           <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
           <ul class="dropdown-menu">
-            <li><a id="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-time"></span> + 1h</a></li>
+            <li><a data-action="edit_selected" data-id="tla" data-api-url='edit/time_limited_alias' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-time"></span> + 1h</a></li>
             <li role="separator" class="divider"></li>
-            <li><a id="delete_selected" data-id="tla" data-api-url='delete/time_limited_alias' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+            <li><a data-action="delete_selected" data-id="tla" data-api-url='delete/time_limited_alias' href="#"><?=$lang['mailbox']['remove'];?></a></li>
           </ul>
         </div>
         <div class="btn-group">
           <a class="btn btn-sm btn-success dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['alias_create_random'];?> <span class="caret"></span></a>
           <ul class="dropdown-menu">
-            <li><a id="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"1"}' href="#">1 <?=$lang['user']['hour'];?></a></li>
-            <li><a id="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"6"}' href="#">6 <?=$lang['user']['hours'];?></a></li>
-            <li><a id="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"24"}' href="#">1 <?=$lang['user']['day'];?></a></li>
-            <li><a id="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"168"}' href="#">1 <?=$lang['user']['week'];?></a></li>
-            <li><a id="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"672"}' href="#">4 <?=$lang['user']['weeks'];?></a></li>
+            <li><a data-action="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"1"}' href="#">1 <?=$lang['user']['hour'];?></a></li>
+            <li><a data-action="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"6"}' href="#">6 <?=$lang['user']['hours'];?></a></li>
+            <li><a data-action="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"24"}' href="#">1 <?=$lang['user']['day'];?></a></li>
+            <li><a data-action="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"168"}' href="#">1 <?=$lang['user']['week'];?></a></li>
+            <li><a data-action="add_item" data-api-url='add/time_limited_alias' data-api-attr='{"validity":"672"}' href="#">4 <?=$lang['user']['weeks'];?></a></li>
           </ul>
         </div>
       </div>
     </div>
-    <?php
-    endif;
-    ?>
+
 	</div>
 
 	<div role="tabpanel" class="tab-pane" id="Spamfilter">
@@ -309,7 +284,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 		<form class="form-horizontal" role="form" data-id="spam_score" method="post">
 			<div class="form-group">
 				<div class="col-lg-6 col-sm-12">
-					<input name="spam_score" id="spam_score" type="text" style="width: 100%;"
+					<input data-acl="<?=$_SESSION['acl']['spam_score'];?>" name="spam_score" id="spam_score" type="text" style="width: 100%;"
 						data-provide="slider"
 						data-slider-min="1"
 						data-slider-max="2000"
@@ -330,21 +305,17 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 					<p><?=$lang['user']['spamfilter_hint'];?></p>
 				</div>
 			</div>
-      <?php
-      if ($_SESSION['acl']['spam_score'] == 1):
-      ?>
+
       <div class="form-group">
 				<div class="col-sm-10">
-        <button type="button" class="btn btn-sm btn-success" id="edit_selected"
+        <button data-acl="<?=$_SESSION['acl']['spam_score'];?>" type="button" class="btn btn-sm btn-success" data-action="edit_selected"
           data-item="<?= htmlentities($username); ?>"
           data-id="spam_score"
           data-api-url='edit/spam-score'
           data-api-attr='{}'><?=$lang['user']['save_changes'];?></button>
 				</div>
 			</div>
-      <?php
-      endif;
-      ?>
+
 		</form>
 		<hr>
 		<div class="row">
@@ -354,26 +325,22 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
         <div class="table-responsive">
           <table class="table table-striped table-condensed" id="wl_policy_mailbox_table"></table>
         </div>
-        <?php
-        if ($_SESSION['acl']['spam_policy'] == 1):
-        ?>
+
         <div class="mass-actions-user">
-          <div class="btn-group">
+          <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
             <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_mailbox" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
-            <a class="btn btn-sm btn-danger" id="delete_selected" data-id="policy_wl_mailbox" data-api-url='delete/mailbox-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+            <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_mailbox" data-api-url='delete/mailbox-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
           </div>
         </div>
         <form class="form-inline" data-id="add_wl_policy_mailbox">
-          <div class="input-group">
+          <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
             <input type="text" class="form-control" name="object_from" id="object_from" placeholder="*@example.org" required>
             <span class="input-group-btn">
-              <button class="btn btn-default" id="add_item" data-id="add_wl_policy_mailbox" data-api-url='add/mailbox-policy' data-api-attr='{"username":<?= json_encode($username); ?>,"object_list":"wl"}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['spamfilter_table_add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_wl_policy_mailbox" data-api-url='add/mailbox-policy' data-api-attr='{"username":<?= json_encode($username); ?>,"object_list":"wl"}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['spamfilter_table_add'];?></button>
             </span>
           </div>
         </form>
-        <?php
-        endif;
-        ?>
+
       </div>
 			<div class="col-sm-6">
 				<h4><?=$lang['user']['spamfilter_bl'];?></h4>
@@ -381,28 +348,22 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
         <div class="table-responsive">
           <table class="table table-striped table-condensed" id="bl_policy_mailbox_table"></table>
         </div>
-        <?php
-        if ($_SESSION['acl']['spam_policy'] == 1):
-        ?>
+
         <div class="mass-actions-user">
-          <div class="btn-group">
+          <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
             <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_mailbox" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
-            <a class="btn btn-sm btn-danger" id="delete_selected" data-id="policy_bl_mailbox" data-api-url='delete/mailbox-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+            <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_mailbox" data-api-url='delete/mailbox-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
           </div>
         </div>
         <form class="form-inline" data-id="add_bl_policy_mailbox">
-          <div class="input-group">
+          <div class="input-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
             <input type="text" class="form-control" name="object_from" id="object_from" placeholder="*@example.org" required>
-            <input type="hidden" name="username" value="<?= htmlentities($username) ;?>">
-            <input type="hidden" name="object_list" value="bl">
             <span class="input-group-btn">
-              <button class="btn btn-default" id="add_item" data-id="add_bl_policy_mailbox" data-api-url='add/mailbox-policy' data-api-attr='{"username":<?= json_encode($username); ?>,"object_list":"bl"}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['spamfilter_table_add'];?></button>
+              <button class="btn btn-default" data-action="add_item" data-id="add_bl_policy_mailbox" data-api-url='add/mailbox-policy' data-api-attr='{"username":<?= json_encode($username); ?>,"object_list":"bl"}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['spamfilter_table_add'];?></button>
             </span>
           </div>
         </form>
-        <?php
-        endif;
-        ?>
+
       </div>
     </div>
   </div>
@@ -411,25 +372,21 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 		<div class="table-responsive">
       <table class="table table-striped" id="sync_job_table"></table>
 		</div>
-    <?php
-    if ($_SESSION['acl']['syncjobs'] == 1):
-    ?>
+
     <div class="mass-actions-user">
-      <div class="btn-group">
+      <div class="btn-group" data-acl="<?=$_SESSION['acl']['syncjobs'];?>">
         <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="syncjob" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
         <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
         <ul class="dropdown-menu">
-          <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-          <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+          <li><a data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+          <li><a data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
           <li role="separator" class="divider"></li>
-          <li><a id="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="syncjob" data-api-url='delete/syncjob' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+          <li><a data-action="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="syncjob" data-api-url='delete/syncjob' href="#"><?=$lang['mailbox']['remove'];?></a></li>
         </ul>
         <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addSyncJobModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['create_syncjob'];?></a>
       </div>
     </div>
-    <?php
-    endif;
-    ?>
+
 		</div>
 	</div>
 
@@ -437,7 +394,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 <div style="margin-bottom:200px;"></div>
 <?php
 }
-if (isset($_SESSION['mailcow_cc_role'])) {
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] != 'admin') {
 require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/user.php';
 ?>
 <script type='text/javascript'>
@@ -450,8 +407,8 @@ echo "var mailcow_cc_username = '". $_SESSION['mailcow_cc_username'] . "';\n";
 echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
 ?>
 </script>
-<script src="js/footable.min.js"></script>
-<script src="js/user.js"></script>
+<script src="/js/footable.min.js"></script>
+<script src="/js/user.js"></script>
 <?php
 require_once("inc/footer.inc.php");
 }

+ 30 - 19
docker-compose.yml

@@ -2,7 +2,7 @@ version: '2.1'
 services:
 
     unbound-mailcow:
-      image: mailcow/unbound:1.1
+      image: mailcow/unbound:1.2
       build: ./data/Dockerfiles/unbound
       command: /usr/sbin/unbound
       environment:
@@ -10,6 +10,7 @@ services:
       volumes:
         - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
       restart: always
+      tty: true
       networks:
         mailcow-network:
           ipv4_address: ${IPV4_NETWORK:-172.22.1}.254
@@ -20,6 +21,7 @@ services:
       image: mariadb:10.2
       volumes:
         - mysql-vol-1:/var/lib/mysql/
+        - mysql-socket-vol-1:/var/run/mysqld/
         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
       environment:
         - TZ=${TZ}
@@ -71,7 +73,7 @@ services:
             - clamd
 
     rspamd-mailcow:
-      image: mailcow/rspamd:1.24
+      image: mailcow/rspamd:1.27
       build: ./data/Dockerfiles/rspamd
       stop_grace_period: 30s
       depends_on:
@@ -79,11 +81,10 @@ services:
       environment:
         - TZ=${TZ}
       volumes:
-        - ./data/conf/rspamd/custom/:/etc/rspamd/custom:ro
-        - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:rw
-        - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:ro
+        - ./data/conf/rspamd/custom/:/etc/rspamd/custom
+        - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d
+        - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d
         - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro
-        - rspamd-sock:/rspamd-sock
         - rspamd-vol-1:/var/lib/rspamd
       restart: always
       dns:
@@ -95,7 +96,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.18
+      image: mailcow/phpfpm:1.21
       build: ./data/Dockerfiles/phpfpm
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
@@ -103,7 +104,8 @@ services:
       volumes:
         - ./data/web:/web:rw
         - ./data/conf/rspamd/dynmaps:/dynmaps:ro
-        - rspamd-sock:/rspamd-sock
+        - rspamd-vol-1:/var/lib/rspamd
+        - mysql-socket-vol-1:/var/run/mysqld/
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro
         - ./data/conf/phpfpm/php-fpm.d/pools.conf:/usr/local/etc/php-fpm.d/z-pools.conf
         - ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini
@@ -136,7 +138,7 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:1.34
+      image: mailcow/sogo:1.38
       build: ./data/Dockerfiles/sogo
       environment:
         - DBNAME=${DBNAME}
@@ -147,6 +149,7 @@ services:
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
       volumes:
         - ./data/conf/sogo/:/etc/sogo/
+        - mysql-socket-vol-1:/var/run/mysqld/
       restart: always
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
@@ -157,7 +160,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.33
+      image: mailcow/dovecot:1.38
       build: ./data/Dockerfiles/dovecot
       cap_add:
         - NET_BIND_SERVICE
@@ -167,13 +170,16 @@ services:
         - ./data/conf/sogo/:/etc/sogo/
         - vmail-vol-1:/var/vmail
         - crypt-vol-1:/mail_crypt/
-        - rspamd-sock:/rspamd-sock
+        - ./data/conf/rspamd/custom/:/etc/rspamd/custom
+        - rspamd-vol-1:/var/lib/rspamd
+        - mysql-socket-vol-1:/var/run/mysqld/
       environment:
         - LOG_LINES=${LOG_LINES:-9999}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
         - TZ=${TZ}
+        - MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-1440}
       ports:
         - "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
         - "${IMAP_PORT:-143}:143"
@@ -196,14 +202,15 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: mailcow/postfix:1.21
+      image: mailcow/postfix:1.23
       build: ./data/Dockerfiles/postfix
       volumes:
         - ./data/conf/postfix:/opt/postfix/conf
         - ./data/assets/ssl:/etc/ssl/mail/:ro
         - postfix-vol-1:/var/spool/postfix
         - crypt-vol-1:/var/lib/zeyple
-        - rspamd-sock:/rspamd-sock
+        - rspamd-vol-1:/var/lib/rspamd
+        - mysql-socket-vol-1:/var/run/mysqld/
       environment:
         - LOG_LINES=${LOG_LINES:-9999}
         - TZ=${TZ}
@@ -264,6 +271,8 @@ services:
         - ./data/assets/ssl/:/etc/ssl/mail/:ro
         - ./data/conf/nginx/:/etc/nginx/conf.d/:rw
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro
+      volumes_from:
+        - sogo-mailcow
       ports:
         - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
         - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
@@ -278,8 +287,7 @@ services:
     acme-mailcow:
       depends_on:
         - nginx-mailcow
-        - mysql-mailcow
-      image: mailcow/acme:1.37
+      image: mailcow/acme:1.38
       build: ./data/Dockerfiles/acme
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
@@ -297,6 +305,7 @@ services:
         - ./data/web/.well-known/acme-challenge:/var/www/acme:rw
         - ./data/assets/ssl:/var/lib/acme/:rw
         - ./data/assets/ssl-example:/var/lib/ssl-example/:ro
+        - mysql-socket-vol-1:/var/run/mysqld/
       restart: always
       networks:
         mailcow-network:
@@ -328,13 +337,14 @@ services:
         - /lib/modules:/lib/modules:ro
 
     watchdog-mailcow:
-      image: mailcow/watchdog:1.19
+      image: mailcow/watchdog:1.24
       # Debug
       #command: /watchdog.sh
       build: ./data/Dockerfiles/watchdog
       oom_kill_disable: true
       volumes:
-        - rspamd-sock:/rspamd-sock
+        - rspamd-vol-1:/var/lib/rspamd
+        - mysql-socket-vol-1:/var/run/mysqld/
       restart: always
       environment:
         - LOG_LINES=${LOG_LINES:-9999}
@@ -354,7 +364,7 @@ services:
             - watchdog
 
     dockerapi-mailcow:
-      image: mailcow/dockerapi:1.13
+      image: mailcow/dockerapi:1.18
       restart: always
       build: ./data/Dockerfiles/dockerapi
       oom_kill_disable: true
@@ -363,6 +373,7 @@ services:
       volumes:
         - /var/run/docker.sock:/var/run/docker.sock:ro
         - ./data/conf/rspamd/override.d/worker-controller-password.inc:/access.inc:rw
+        - vmail-vol-1:/var/vmail:ro
       networks:
         mailcow-network:
           aliases:
@@ -390,8 +401,8 @@ networks:
 volumes:
   vmail-vol-1:
   mysql-vol-1:
+  mysql-socket-vol-1:
   redis-vol-1:
   rspamd-vol-1:
   postfix-vol-1:
   crypt-vol-1:
-  rspamd-sock:

+ 6 - 0
generate_config.sh

@@ -104,6 +104,12 @@ TZ=${MAILCOW_TZ}
 # Fixed project name
 COMPOSE_PROJECT_NAME=mailcowdockerized
 
+# Garbage collector cleanup
+# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring
+# How long should objects remain in the garbage until they are being deleted? (value in minutes)
+# Check interval is hourly
+MAILDIR_GC_TIME=1440
+
 # Additional SAN for the certificate
 ADDITIONAL_SAN=
 

+ 21 - 3
helper-scripts/backup_and_restore.sh

@@ -5,8 +5,8 @@ if [[ ! ${1} =~ (backup|restore) ]]; then
   exit 1
 fi
 
-if [[ ${1} == "backup" && ! ${2} =~ (vmail|redis|rspamd|postfix|mysql|all) ]]; then
-  echo "Second parameter needs to be 'vmail', 'redis', 'rspamd', 'postfix', 'mysql' or 'all'"
+if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all) ]]; then
+  echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql' or 'all'"
   exit 1
 fi
 
@@ -62,6 +62,12 @@ function backup() {
         -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail \
         debian:stretch-slim /bin/tar --warning='no-file-ignored' -Pcvpzf /backup/backup_vmail.tar.gz /vmail
       ;;&
+    crypt|all)
+      docker run --rm \
+        -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt \
+        debian:stretch-slim /bin/tar --warning='no-file-ignored' -Pcvpzf /backup/backup_crypt.tar.gz /crypt
+      ;;&
     redis|all)
       docker exec $(docker ps -qf name=redis-mailcow) redis-cli save
       docker run --rm \
@@ -128,6 +134,14 @@ function restore() {
         debian:stretch-slim /bin/tar -Pxvzf /backup/backup_redis.tar.gz
       docker start $(docker ps -aqf name=redis-mailcow)
       ;;
+    crypt)
+      docker stop $(docker ps -qf name=dovecot-mailcow)
+      docker run -it --rm \
+        -v ${RESTORE_LOCATION}:/backup \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt \
+        debian:stretch-slim /bin/tar -Pxvzf /backup/backup_crypt.tar.gz
+      docker start $(docker ps -aqf name=dovecot-mailcow)
+      ;;
     rspamd)
       docker stop $(docker ps -qf name=rspamd-mailcow)
       docker run -it --rm \
@@ -189,7 +203,7 @@ elif [[ ${1} == "restore" ]]; then
   echo
   declare -A FILE_SELECTION
   RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}"
-  if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 -type f -regex ".*\(redis\|rspamd\|mysql\|vmail\|postfix\).*") ]]; then
+  if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 -type f -regex ".*\(redis\|rspamd\|mysql\|crypt\|vmail\|postfix\).*") ]]; then
     echo "No datasets found"
     exit 1
   fi
@@ -198,6 +212,10 @@ elif [[ ${1} == "restore" ]]; then
       echo "[ ${i} ] - Mail directory (/var/vmail)"
       FILE_SELECTION[${i}]="vmail"
       ((i++))
+    elif [[ ${file} =~ crypt ]]; then
+      echo "[ ${i} ] - Crypt data"
+      FILE_SELECTION[${i}]="crypt"
+      ((i++))
     elif [[ ${file} =~ redis ]]; then
       echo "[ ${i} ] - Redis DB"
       FILE_SELECTION[${i}]="redis"

+ 31 - 0
helper-scripts/ext_sql_sock.docker-compose.override.yml

@@ -0,0 +1,31 @@
+version: '2.1'
+services:
+
+    php-fpm-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    sogo-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    dovecot-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    postfix-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    acme-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    watchdog-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    mysql-mailcow:
+      image: alpine:3.8
+      command: /bin/true
+      restart: "no"

+ 1 - 1
helper-scripts/nextcloud.sh

@@ -64,7 +64,7 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
 
 	ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
 
-	curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-13.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
+	curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-14.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
 	  && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
 	  && rm nextcloud.tar.bz2 \
 	  && rm -rf ./data/web/nextcloud/updater \

+ 11 - 1
update.sh

@@ -12,7 +12,6 @@ DATE=$(date +%Y-%m-%d_%H_%M_%S)
 BRANCH=$(git rev-parse --abbrev-ref HEAD)
 
 docker_garbage() {
-  echo -e "\e[32mCollecting garbage...\e[0m"
   IMGS_TO_DELETE=()
   for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do
     REPOSITORY=${container/:*}
@@ -74,6 +73,7 @@ while (($#)); do
       MERGE_STRATEGY=ours
     ;;
     --gc)
+      echo -e "\e[32mCollecting garbage...\e[0m"
       docker_garbage
       exit 0
     ;;
@@ -119,6 +119,7 @@ CONFIG_ARRAY=(
   "SQL_PORT"
   "API_KEY"
   "API_ALLOW_FROM"
+  "MAILDIR_GC_TIME"
 )
 
 sed -i '$a\' mailcow.conf
@@ -200,6 +201,15 @@ for option in ${CONFIG_ARRAY[@]}; do
       echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf
       echo "#SNAT6_TO_SOURCE=" >> mailcow.conf
     fi
+  elif [[ ${option} == "MAILDIR_GC_TIME" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo '# Garbage collector cleanup' >> mailcow.conf
+      echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf
+      echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf
+      echo '# Check interval is hourly' >> mailcow.conf
+      echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf
+    fi
   elif ! grep -q ${option} mailcow.conf; then
     echo "Adding new option \"${option}\" to mailcow.conf"
     echo "${option}=n" >> mailcow.conf