Browse Source

Merge pull request #887 from mailcow/dev

Dev to master
André Peters 7 years ago
parent
commit
079170682c
100 changed files with 7144 additions and 1802 deletions
  1. 2 1
      .gitignore
  2. 1 1
      .travis.yml
  3. 7 6
      data/Dockerfiles/acme/Dockerfile
  4. 26 15
      data/Dockerfiles/acme/docker-entrypoint.sh
  5. 18 2
      data/Dockerfiles/dockerapi/server.py
  6. 20 9
      data/Dockerfiles/dovecot/Dockerfile
  7. 321 344
      data/Dockerfiles/dovecot/imapsync
  8. 1 1
      data/Dockerfiles/dovecot/syslog-ng.conf
  9. 2 2
      data/Dockerfiles/phpfpm/Dockerfile
  10. 27 1
      data/Dockerfiles/postfix/postfix.sh
  11. 1 1
      data/Dockerfiles/postfix/syslog-ng.conf
  12. 4 3
      data/Dockerfiles/sogo/bootstrap-sogo.sh
  13. 1 1
      data/Dockerfiles/sogo/syslog-ng.conf
  14. 9 43
      data/Dockerfiles/watchdog/watchdog.sh
  15. 12 4
      data/conf/dovecot/dovecot.conf
  16. 7 0
      data/conf/mysql/my.cnf
  17. 19 0
      data/conf/nginx/meta_exporter.conf
  18. 27 2
      data/conf/postfix/main.cf
  19. 5 0
      data/conf/postfix/master.cf
  20. 0 1
      data/conf/rspamd/custom/.empty
  21. 1 1
      data/conf/rspamd/dynmaps/settings.php
  22. 6 2
      data/conf/rspamd/local.d/composites.conf
  23. 0 15
      data/conf/rspamd/local.d/force_actions.conf
  24. 1 2
      data/conf/rspamd/local.d/statistic.conf
  25. 0 0
      data/conf/rspamd/lua/ratelimit.lua
  26. 155 0
      data/conf/rspamd/meta_exporter/pipe.php
  27. 3 0
      data/conf/rspamd/meta_exporter/vars.inc.php
  28. 1 0
      data/conf/rspamd/override.d/logging.inc
  29. 1 1
      data/conf/rspamd/override.d/ratelimit.conf
  30. 1 0
      data/conf/rspamd/override.d/worker-controller-password.inc
  31. 2 1
      data/conf/rspamd/override.d/worker-controller.inc
  32. 1 0
      data/conf/rspamd/override.d/worker-normal.inc
  33. 1 0
      data/conf/rspamd/override.d/worker-proxy.inc
  34. 104 131
      data/web/admin.php
  35. 1 0
      data/web/css/breakpoint.min.css
  36. 37 0
      data/web/css/debug.css
  37. 9 1
      data/web/css/mailbox.css
  38. 37 0
      data/web/css/quarantaine.css
  39. 328 0
      data/web/debug.php
  40. 0 241
      data/web/diagnostics.php
  41. 48 4
      data/web/edit.php
  42. BIN
      data/web/img/rspamd_logo.png
  43. 53 0
      data/web/inc/ajax/container_ctrl.php
  44. 402 0
      data/web/inc/ajax/dns_diagnostics.php
  45. 12 0
      data/web/inc/ajax/log_driver.php
  46. 83 0
      data/web/inc/ajax/qitem_details.php
  47. 0 39
      data/web/inc/ajax/sogo_ctrl.php
  48. 69 64
      data/web/inc/footer.inc.php
  49. 292 0
      data/web/inc/functions.bcc.inc.php
  50. 45 10
      data/web/inc/functions.customize.inc.php
  51. 16 6
      data/web/inc/functions.docker.inc.php
  52. 1 1
      data/web/inc/functions.fail2ban.inc.php
  53. 203 10
      data/web/inc/functions.inc.php
  54. 44 20
      data/web/inc/functions.mailbox.inc.php
  55. 282 0
      data/web/inc/functions.quarantaine.inc.php
  56. 2 2
      data/web/inc/functions.relayhost.inc.php
  57. 11 3
      data/web/inc/header.inc.php
  58. 89 5
      data/web/inc/init_db.inc.php
  59. 2 1
      data/web/inc/lib/composer.json
  60. 81 1
      data/web/inc/lib/composer.lock
  61. 1 0
      data/web/inc/lib/vendor/composer/autoload_psr4.php
  62. 8 0
      data/web/inc/lib/vendor/composer/autoload_static.php
  63. 82 0
      data/web/inc/lib/vendor/composer/installed.json
  64. 21 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/LICENSE
  65. 167 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/README.md
  66. 61 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/composer.json
  67. 303 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/mailparse-stubs.php
  68. 6 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/phpunit.xml.dist
  69. 183 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Attachment.php
  70. 338 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Charset.php
  71. 24 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Contracts/CharsetManager.php
  72. 8 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Exception.php
  73. 893 0
      data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Parser.php
  74. 22 5
      data/web/inc/prerequisites.inc.php
  75. 18 0
      data/web/inc/sessions.inc.php
  76. 12 0
      data/web/inc/triggers.inc.php
  77. 10 8
      data/web/inc/vars.inc.php
  78. 17 11
      data/web/index.php
  79. 1 349
      data/web/js/admin.js
  80. 41 9
      data/web/js/api.js
  81. 503 0
      data/web/js/debug.js
  82. 9 0
      data/web/js/formcache.min.js
  83. 90 11
      data/web/js/mailbox.js
  84. 82 0
      data/web/js/quarantaine.js
  85. 450 353
      data/web/js/u2f-api.js
  86. 4 4
      data/web/js/user.js
  87. 416 1
      data/web/json_api.php
  88. 92 5
      data/web/lang/lang.de.php
  89. 72 4
      data/web/lang/lang.en.php
  90. 28 0
      data/web/mailbox.php
  91. 2 2
      data/web/modals/admin.php
  92. 6 0
      data/web/modals/debug.php
  93. 7 7
      data/web/modals/footer.php
  94. 93 16
      data/web/modals/mailbox.php
  95. 32 0
      data/web/modals/quarantaine.php
  96. 2 2
      data/web/modals/user.php
  97. 56 0
      data/web/quarantaine.php
  98. 12 5
      data/web/user.php
  99. 36 12
      docker-compose.yml
  100. 2 0
      generate_config.sh

+ 2 - 1
.gitignore

@@ -17,8 +17,9 @@ data/conf/rspamd/local.d/*
 data/conf/rspamd/override.d/*
 !data/conf/nginx/dynmaps.conf
 !data/conf/nginx/site.conf
+!data/conf/nginx/meta_exporter.conf
 data/conf/nginx/*.conf
 data/conf/nginx/*.custom
 data/conf/nginx/*.bak
 data/conf/dovecot/extra.conf
-data/conf/rspamd/custom/*
+data/conf/rspamd/override.d/worker-controller-password.inc

+ 1 - 1
.travis.yml

@@ -9,7 +9,7 @@ script:
 - docker-compose push
 branches:
   only:
-  - dev
+  - master
 env:
   global:
   - secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w=

+ 7 - 6
data/Dockerfiles/acme/Dockerfile

@@ -3,20 +3,21 @@ FROM alpine:3.6
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 RUN apk add --update --no-cache \
-	bash \
-	curl \
-	openssl \
-	bind-tools \
-	jq \
+  bash \
+  curl \
+  openssl \
+  bind-tools \
+  jq \
   libressl-dev \
   libbsd-dev \
   libseccomp-dev \
-	mariadb-client \
+  mariadb-client \
   tini \
   make \
   gcc \
   libressl \
   libc-dev \
+  redis \
   linux-headers \
   ca-certificates \
   && curl -s https://kristaps.bsd.lv/acme-client/snapshots/acme-client-portable.tgz | tar xfvz - \

+ 26 - 15
data/Dockerfiles/acme/docker-entrypoint.sh

@@ -2,16 +2,30 @@
 set -o pipefail
 exec 5>&1
 
+log_f() {
+  if [[ ${2} == "no_nl" ]]; then
+    echo -n "$(date) - ${1}"
+  elif [[ ${2} == "no_date" ]]; then
+    echo "${1}"
+  elif [[ ${2} != "redis_only" ]]; then
+    echo "$(date) - ${1}"
+  fi
+  redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
+    tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null
+  redis-cli -h redis LTRIM ACME_LOG 0 ${LOG_LINES} > /dev/null
+}
+
 if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
   log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
   sleep 365d
   exec $(readlink -f "$0")
 fi
 
-echo "Waiting for Docker API..."
+log_f "Waiting for Docker API..." no_nl
 until ping dockerapi -c1 > /dev/null; do
   sleep 1
 done
+log_f "Found Docker API" no_date
 
 ACME_BASE=/var/lib/acme
 SSL_EXAMPLE=/var/lib/ssl-example
@@ -20,21 +34,12 @@ mkdir -p ${ACME_BASE}/acme/private
 
 restart_containers(){
   for container in $*; do
-    echo "Restarting ${container}..."
-    curl -X POST http://dockerapi:8080/containers/${container}/restart
+    log_f "Restarting ${container}..." no_nl
+    C_REST_OUT=$(curl -X POST http://dockerapi:8080/containers/${container}/restart | jq -r '.msg')
+    log_f "${C_REST_OUT}" no_date
   done
 }
 
-log_f() {
-  if [[ ${2} == "no_nl" ]]; then
-    echo -n "$(date) - ${1}"
-  elif [[ ${2} == "no_date" ]]; then
-    echo "${1}"
-  else
-    echo "$(date) - ${1}"
-  fi
-}
-
 array_diff() {
   # https://stackoverflow.com/questions/2312762, Alex Offshore
   eval local ARR1=\(\"\${$2[@]}\"\)
@@ -127,12 +132,13 @@ while true; do
   # 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" " "))
 
-  log_f "Waiting for domain tables... " no_nl
+  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)
     [[ -z ${DOMAIN_TABLE} ]] && sleep 10
   done
-  log_f "OK" no_date
+  log_f "Found domain tables." no_date
 
   while read domains; do
     SQL_DOMAIN_ARR+=("${domains}")
@@ -226,6 +232,7 @@ while true; do
 
   case "$?" in
     0) # new certs
+      log_f "${ACME_RESPONSE}" redis_only
       # cp the new certificates and keys
       cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
       cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
@@ -239,6 +246,7 @@ while true; do
       restart_containers ${CONTAINERS_RESTART[*]}
       ;;
     1) # failure
+      log_f "${ACME_RESPONSE}" redis_only
       if [[ $ACME_RESPONSE =~ "No registration exists" ]]; then
         log_f "Registration keys are invalid, deleting old keys and restarting..."
         rm ${ACME_BASE}/acme/private/account.key
@@ -268,6 +276,7 @@ while true; do
       exec $(readlink -f "$0")
       ;;
     2) # no change
+      log_f "${ACME_RESPONSE}" redis_only
       if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then
         log_f "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..."
         cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
@@ -280,9 +289,11 @@ while true; do
         cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
         TRIGGER_RESTART=1
       fi
+      log_f "Certificate was not changed"
       [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
       ;;
     *) # unspecified
+      log_f "${ACME_RESPONSE}" redis_only
       if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
         log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...."
         cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem

+ 18 - 2
data/Dockerfiles/dockerapi/server.py

@@ -6,8 +6,11 @@ from threading import Thread
 import docker
 import signal
 import time
+import os
+import re
+import sys
 
-docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock')
+docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
 app = Flask(__name__)
 api = Api(app)
 
@@ -76,6 +79,18 @@ class container_post(Resource):
               return container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"], user='vmail')
           except Exception as e:
             return jsonify(type='danger', msg=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("'", "'\\''") + "'"], user='_rspamd')
+              f = open("/access.inc", "w")
+              f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash.rstrip()) + '";\n')
+              f.close()
+              container.restart()
+              return jsonify(type='success', msg='command completed successfully')
+          except Exception as e:
+            return jsonify(type='danger', msg=e)
+
         else:
           return jsonify(type='danger', msg='Unknown command')
 
@@ -95,7 +110,7 @@ class GracefulKiller:
     self.kill_now = True
 
 def startFlaskAPI():
-  app.run(debug=False, host='0.0.0.0', port='8080', threaded=True)
+  app.run(debug=False, host='0.0.0.0', port=8080, threaded=True)
 
 api.add_resource(containers_get, '/containers/json')
 api.add_resource(container_get, '/containers/<string:container_id>/json')
@@ -111,3 +126,4 @@ if __name__ == '__main__':
     if killer.kill_now:
       break
   print "Stopping dockerapi-mailcow"
+

+ 20 - 9
data/Dockerfiles/dovecot/Dockerfile

@@ -3,10 +3,10 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
 ENV LC_ALL C
-ENV DOVECOT_VERSION 2.2.33.2
-ENV PIGEONHOLE_VERSION 0.4.21
+ENV DOVECOT_VERSION 2.3.0
+ENV PIGEONHOLE_VERSION 0.5.0.1
 
-RUN apt-get update && apt-get -y install \
+RUN apt-get update && apt-get -y --no-install-recommends install \
 	automake \
 	autotools-dev \
 	build-essential \
@@ -14,6 +14,16 @@ RUN apt-get update && apt-get -y install \
 	cpanminus \
 	curl \
 	default-libmysqlclient-dev \
+  libjson-webtoken-perl \
+  libcgi-pm-perl \
+  libcrypt-openssl-rsa-perl \
+  libdata-uniqid-perl \
+  libhtml-parser-perl \
+  libmail-imapclient-perl \
+  libparse-recdescent-perl \
+  libsys-meminfo-perl \
+  libtest-mockobject-perl \
+  libwww-perl \
 	libauthen-ntlm-perl \
 	libbz2-dev \
 	libcrypt-ssleay-perl \
@@ -46,27 +56,28 @@ RUN apt-get update && apt-get -y install \
 	make \
   procps \
 	supervisor \
+  cron \
 	syslog-ng \
 	syslog-ng-core \
 	syslog-ng-mod-redis \
 	&& rm -rf /var/lib/apt/lists/*
 
 
-RUN curl https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz | tar xvz  \
-	&& cd dovecot-$DOVECOT_VERSION \
+RUN curl https://www.dovecot.org/releases/2.3/dovecot-$DOVECOT_VERSION.tar.gz | tar xvz  \
+	&& cd dovecot-ce-$DOVECOT_VERSION \
 	&& ./configure --with-mysql --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib \
 	&& make -j3 \
 	&& make install \
 	&& make clean \
-	&& cd .. && rm -rf dovecot-$DOVECOT_VERSION \
-	&& curl https://pigeonhole.dovecot.org/releases/2.2/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION.tar.gz | tar xvz  \
-	&& cd dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \
+	&& cd .. && rm -rf dovecot-ce-$DOVECOT_VERSION \
+	&& curl https://pigeonhole.dovecot.org/releases/2.3/dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION.tar.gz | tar xvz  \
+	&& cd dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION \
 	&& ./configure \
 	&& make -j3 \
 	&& make install \
 	&& make clean \
 	&& cd .. \
-  && rm -rf dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION
+  && 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

File diff suppressed because it is too large
+ 321 - 344
data/Dockerfiles/dovecot/imapsync


+ 1 - 1
data/Dockerfiles/dovecot/syslog-ng.conf

@@ -35,7 +35,7 @@ destination d_redis_cleanup {
     host("redis-mailcow")
     persist-name("redis3")
     port(6379)
-    command("LTRIM" "DOVECOT_MAILLOG" "0" "9999")
+    command("LTRIM" "DOVECOT_MAILLOG" "0" "`LOG_LINES`")
   );
 };
 filter f_mail { facility(mail); };

+ 2 - 2
data/Dockerfiles/phpfpm/Dockerfile

@@ -41,8 +41,8 @@ RUN apk add -U --no-cache libxml2-dev \
     Net_Sieve \
     NET_SMTP \
     Mail_mime \
-	&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \
-	&& docker-php-ext-enable redis apcu memcached imagick \
+	&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} mailparse \
+	&& docker-php-ext-enable redis apcu memcached imagick mailparse \
 	&& pecl clear-cache \
 	&& docker-php-ext-configure intl \
   && docker-php-ext-install -j 4 intl gettext ldap sockets soap pdo pdo_mysql xmlrpc gd zip pcntl opcache \

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

@@ -83,7 +83,11 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
   WHERE id IN (
     SELECT relayhost FROM domain
       WHERE CONCAT('@', domain) = '%s'
-  );
+      OR '%s' IN (
+        SELECT CONCAT('@', alias_domain) FROM alias_domain
+      )
+  )
+  AND username != '';
 EOF
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
@@ -119,6 +123,28 @@ query = SELECT goto FROM alias
     AND active='1';
 EOF
 
+cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
+user = ${DBUSER}
+password = ${DBPASS}
+hosts = mysql
+dbname = ${DBNAME}
+query = SELECT bcc_dest FROM bcc_maps
+  WHERE local_dest='%s'
+    AND type='rcpt'
+    AND active='1';
+EOF
+
+cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
+user = ${DBUSER}
+password = ${DBPASS}
+hosts = mysql
+dbname = ${DBNAME}
+query = SELECT bcc_dest FROM bcc_maps
+  WHERE local_dest='%s'
+    AND type='sender'
+    AND active='1';
+EOF
+
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
 user = ${DBUSER}
 password = ${DBPASS}

+ 1 - 1
data/Dockerfiles/postfix/syslog-ng.conf

@@ -35,7 +35,7 @@ destination d_redis_cleanup {
     host("redis-mailcow")
     persist-name("redis3")
     port(6379)
-    command("LTRIM" "POSTFIX_MAILLOG" "0" "9999")
+    command("LTRIM" "POSTFIX_MAILLOG" "0" "`LOG_LINES`")
   );
 };
 filter f_mail { facility(mail); };

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

@@ -1,7 +1,10 @@
 #!/bin/bash
 
 # Wait for MySQL to warm-up
-while mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS}${DBPASS} --silent; do
+while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do
+  echo "Waiting for database to come up..."
+  sleep 2
+done
 
 # Wait until port becomes free and send sig
 until ! nc -z sogo-mailcow 20000;
@@ -101,5 +104,3 @@ chown sogo:sogo -R /var/lib/sogo/
 chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
 
 exec gosu sogo /usr/sbin/sogod
-
-done

+ 1 - 1
data/Dockerfiles/sogo/syslog-ng.conf

@@ -38,7 +38,7 @@ destination d_redis_cleanup {
     host("redis-mailcow")
     persist-name("redis3")
     port(6379)
-    command("LTRIM" "SOGO_LOG" "0" "9999")
+    command("LTRIM" "SOGO_LOG" "0" "`LOG_LINES`")
   );
 };
 log {

+ 9 - 43
data/Dockerfiles/watchdog/watchdog.sh

@@ -28,21 +28,19 @@ progress() {
   [[ ${CURRENT} -gt ${TOTAL} ]] && return
   [[ ${CURRENT} -lt 0 ]] && CURRENT=0
   PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} ))
-  log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}"
-  log_data "$(printf "%d,%d,%d,%d" ${PERCENT} ${CURRENT} ${TOTAL} ${DIFF})" "${SERVICE}"
+  redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null
+  log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_redis
 }
 
 log_msg() {
-	redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}")\"}" > /dev/null
+  if [[ ${2} != "no_redis" ]]; then
+    redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
+      tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null
+  fi
+  redis-cli -h redis LTRIM WATCHDOG_LOG 0 ${LOG_LINES} > /dev/null
   echo $(date) $(printf '%s\n' "${1}")
 }
 
-log_data() {
-  [[ -z ${1} ]] && return 1
-  [[ -z ${2} ]] && return 2
-	redis-cli -h redis LPUSH WATCHDOG_DATA "{\"time\":\"$(date +%s)\",\"service\":\"data\",\"$(printf '%s' "${2}")\":\"$(printf '%s' "${1}")\"}" > /dev/null
-}
-
 function mail_error() {
   [[ -z ${1} ]] && return 1
   [[ -z ${2} ]] && return 2
@@ -149,7 +147,7 @@ postfix_checks() {
   while [ ${err_count} -lt ${THRESHOLD} ]; do
 	host_ip=$(get_container_ip postfix-mailcow)
     err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f watchdog -C "RCPT TO:null@localhost" -C DATA -C . -R 250 1>&2; err_count=$(( ${err_count} + $? ))
+    /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f "watchdog@invalid" -C "RCPT TO:null@localhost" -C DATA -C . -R 250 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -S 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} ))
@@ -169,7 +167,7 @@ dovecot_checks() {
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     host_ip=$(get_container_ip dovecot-mailcow)
     err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 24 -f "watchdog" -C "RCPT TO:<watchdog@invalid>" -L -R "User doesn't exist" 1>&2; err_count=$(( ${err_count} + $? ))
+    /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 24 -f "watchdog@invalid" -C "RCPT TO:<watchdog@invalid>" -L -R "User doesn't exist" 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 993 -S -e "OK " 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 143 -e "OK " 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10001 -e "VERSION" 1>&2; err_count=$(( ${err_count} + $? ))
@@ -234,27 +232,6 @@ Empty
   return 1
 }
 
-dns_checks() {
-  err_count=0
-  diff_c=0
-  THRESHOLD=28
-  # Reduce error count by 2 after restarting an unhealthy container
-  trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
-  while [ ${err_count} -lt ${THRESHOLD} ]; do
-    host_ip=$(get_container_ip unbound-mailcow)
-    err_c_cur=${err_count}
-    /usr/lib/nagios/plugins/check_dns -H google.com 1>&2; err_count=$(( ${err_count} + ($? * 2)))
-    /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H google.com 1>&2; err_count=$(( ${err_count} + ($? * 2)))
-    dig +dnssec org. @${host_ip} | grep -E 'flags:.+ad' 1>&2; err_count=$(( ${err_count} + ($? * 2)))
-    [ ${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 "Unbound" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
-    diff_c=0
-    sleep $(( ( RANDOM % 30 )  + 10 ))
-  done
-  return 1
-}
-
 # Create watchdog agents
 (
 while true; do
@@ -322,17 +299,6 @@ done
 ) &
 BACKGROUND_TASKS+=($!)
 
-(
-while true; do
-  if ! dns_checks; then
-    log_msg "Unbound hit error limit"
-    [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "unbound-mailcow"
-    #echo unbound-mailcow > /tmp/com_pipe
-  fi
-done
-) &
-BACKGROUND_TASKS+=($!)
-
 (
 while true; do
   if ! rspamd_checks; then

+ 12 - 4
data/conf/dovecot/dovecot.conf

@@ -14,12 +14,16 @@ 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
-ssl_protocols = !SSLv3
+# Dovecot 2.2
+#ssl_protocols = !SSLv3
+# Dovecot 2.3
+ssl_min_protocol = TLSv1
 ssl_prefer_server_ciphers = yes
 ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA
+# Default in Dovecot 2.3
 ssl_options = no_compression
-# Automatically regenerates every week
-ssl_dh_parameters_length = 2048
+# New in Dovecot 2.3
+ssl_dh=</etc/ssl/mail/dhparams.pem
 log_timestamp = "%Y-%m-%d %H:%M:%S "
 recipient_delimiter = +
 auth_master_user_separator = *
@@ -55,6 +59,9 @@ namespace inbox {
   mailbox "Deleted Items" {
     special_use = \Trash
   }
+  mailbox "Rubbish" {
+    special_use = \Trash
+  }
   mailbox "Gelöschte Objekte" {
     special_use = \Trash
   }
@@ -163,7 +170,7 @@ namespace {
     prefix = Shared/%%u/
     location = maildir:%%h/:INDEXPVT=~/Shared/%%u
     subscriptions = no
-    list = yes
+    list = children
 }
 protocols = imap sieve lmtp pop3
 service dict {
@@ -257,6 +264,7 @@ plugin {
   sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
   sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
   sieve_max_script_size = 1M
+  sieve_max_redirects = 30
   sieve_quota_max_scripts = 0
   sieve_quota_max_storage = 0
   listescape_char = "\\"

+ 7 - 0
data/conf/mysql/my.cnf

@@ -6,6 +6,13 @@ innodb_file_per_table          = TRUE
 innodb_file_format             = barracuda
 innodb_large_prefix            = TRUE
 #sql_mode=IGNORE_SPACE,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
+max_allowed_packet=192M
+max-connections=1500
+innodb-strict-mode=0
+skip-host-cache
+skip-name-resolve
+log-warnings=0
+event_scheduler=1
 
 [client]
 default-character-set = utf8mb4

+ 19 - 0
data/conf/nginx/meta_exporter.conf

@@ -0,0 +1,19 @@
+server {
+  listen 9081;
+  index index.php index.html;
+  server_name _;
+  error_log  /var/log/nginx/error.log;
+  access_log /var/log/nginx/access.log;
+  root /meta_exporter;
+  client_max_body_size 10M;
+  location ~ \.php$ {
+    client_max_body_size 10M;
+    try_files $uri =404;
+    fastcgi_split_path_info ^(.+\.php)(/.+)$;
+    fastcgi_pass phpfpm:9000;
+    fastcgi_index pipe.php;
+    include fastcgi_params;
+    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+    fastcgi_param PATH_INFO $fastcgi_path_info;
+  }
+}

+ 27 - 2
data/conf/postfix/main.cf

@@ -39,7 +39,27 @@ postscreen_greet_ttl = 2d
 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_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, $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $smtpd_sender_login_maps
+proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.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,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf,
+  $local_recipient_maps,
+  $mydestination,
+  $virtual_alias_maps,
+  $virtual_alias_domains,
+  $virtual_mailbox_maps,
+  $virtual_mailbox_domains,
+  $relay_recipient_maps,
+  $relay_domains,
+  $canonical_maps,
+  $sender_canonical_maps,
+  $recipient_canonical_maps,
+  $relocated_maps,
+  $transport_maps,
+  $mynetworks,
+  $smtpd_sender_login_maps
 queue_run_delay = 300s
 relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
 relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
@@ -79,10 +99,15 @@ smtpd_tls_mandatory_ciphers = high
 smtpd_tls_security_level = may
 tls_ssl_options = NO_COMPRESSION
 tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA
-virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
+virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
 virtual_gid_maps = static:5000
 virtual_mailbox_base = /var/vmail/
 virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
+recipient_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
+sender_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
 virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
 virtual_minimum_uid = 104
 virtual_transport = lmtp:inet:dovecot:24

+ 5 - 0
data/conf/postfix/master.cf

@@ -12,6 +12,11 @@ submission inet n       -       n       -       -       smtpd
 588 inet n      -       n       -       -       smtpd
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
   -o smtpd_tls_auth_only=no
+590 inet n      -       n       -       -       smtpd
+  -o smtpd_client_restrictions=permit_mynetworks,reject
+  -o smtpd_tls_auth_only=no
+  -o smtpd_milters=
+  -o non_smtpd_milters=
 smtp_enforced_tls      unix  -       -       n       -       -       smtp
   -o smtp_tls_security_level=encrypt
   -o syslog_name=enforced-tls-smtp

+ 0 - 1
data/conf/rspamd/custom/.empty

@@ -1 +0,0 @@
-1

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

@@ -17,7 +17,7 @@ $opt = [
 ];
 try {
   $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
-  $stmt = $pdo->query("SELECT * FROM `filterconf`");
+  $stmt = $pdo->query("SELECT '1' FROM `filterconf`");
 }
 catch (PDOException $e) {
   echo 'settings { }';

+ 6 - 2
data/conf/rspamd/local.d/composites.conf

@@ -1,4 +1,8 @@
 MX_IMPLICIT {
-    expression = "MX_GOOD and MX_MISSING";
-    score = -0.01;
+  expression = "MX_GOOD and MX_MISSING";
+  score = -0.01;
+}
+VIRUS_FOUND {
+  expression = "CLAM_VIRUS & !MAILCOW_WHITE";
+  score = 2000;
 }

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

@@ -1,14 +1,4 @@
 rules {
-  DKIM_FAIL {
-    action = "add header";
-    expression = "R_DKIM_REJECT & !MAILLIST & !MAILCOW_WHITE & !MAILCOW_BLACK";
-    require_action = ["no action", "greylist", "soft reject"];
-  }
-  VIRUS_FOUND {
-    action = "reject";
-    expression = "CLAM_VIRUS & !MAILCOW_WHITE";
-    honor_action = ["reject"];
-  }
   WHITELIST_FORWARDING_HOST_NO_REJECT {
     action = "add header";
     expression = "WHITELISTED_FWD_HOST";
@@ -19,9 +9,4 @@ rules {
     expression = "WHITELISTED_FWD_HOST";
     require_action = ["greylist", "soft reject"];
   }
-  ADD_UNAUTH_SUBJ {
-    action = "rewrite subject";
-    subject = "[Unauth] %s";
-    expression = "SPOOFED_SENDER";
-  }
 }

+ 1 - 2
data/conf/rspamd/local.d/statistic.conf

@@ -7,8 +7,7 @@ classifier "bayes" {
     servers = "redis:6379";
     min_tokens = 11;
     min_learns = 20;
-    autolearn = true;
-
+    autolearn = [-20, 50];
     per_user = <<EOD
 return function(task)
     local rcpt = task:get_recipients(1)

+ 0 - 0
data/conf/rspamd/custom/ratelimit.lua → data/conf/rspamd/lua/ratelimit.lua


+ 155 - 0
data/conf/rspamd/meta_exporter/pipe.php

@@ -0,0 +1,155 @@
+<?php
+// File size is limited by Nginx site to 10M
+// To speed things up, we do not include prerequisites
+header('Content-Type: text/plain');
+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;
+$opt = [
+    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
+    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+    PDO::ATTR_EMULATE_PREPARES   => false,
+];
+try {
+  $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
+}
+catch (PDOException $e) {
+  http_response_code(501);
+  exit;
+}
+// Init Redis
+$redis = new Redis();
+$redis->connect('redis-mailcow', 6379);
+
+// Functions
+function parse_email($email) {
+  if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
+  $a = strrpos($email, '@');
+  return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
+}
+if (!function_exists('getallheaders'))  {
+  function getallheaders() {
+    if (!is_array($_SERVER)) {
+      return array();
+    }
+    $headers = array();
+    foreach ($_SERVER as $name => $value) {
+      if (substr($name, 0, 5) == 'HTTP_') {
+        $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
+      }
+    }
+    return $headers;
+  }
+}
+
+$raw_data = file_get_contents('php://input');
+$headers = getallheaders();
+
+$qid      = $headers['X-Rspamd-Qid'];
+$score    = $headers['X-Rspamd-Score'];
+$rcpts    = $headers['X-Rspamd-Rcpt'];
+$user     = $headers['X-Rspamd-User'];
+$ip       = $headers['X-Rspamd-Ip'];
+$action   = $headers['X-Rspamd-Action'];
+$sender   = $headers['X-Rspamd-From'];
+$symbols  = $headers['X-Rspamd-Symbols'];
+
+$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
+
+try {
+  if ($max_size = $redis->Get('Q_MAX_SIZE')) {
+    if (!empty($max_size) && ($max_size * 1048576) < $raw_size) {
+      error_log(sprintf("Message too large: %d exceeds %d", $raw_size, ($max_size * 1048576)));
+      http_response_code(505);
+      exit;
+    }
+  }
+  if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) {
+    $exclude_domains = json_decode($exclude_domains, true);
+  }
+  $retention_size = (int)$redis->Get('Q_RETENTION_SIZE');
+}
+catch (RedisException $e) {
+  error_log($e);
+  http_response_code(504);
+  exit;
+}
+
+$filtered_rcpts = array();
+foreach (json_decode($rcpts, true) as $rcpt) {
+  $parsed_mail = parse_email($rcpt);
+  if (in_array($parsed_mail['domain'], $exclude_domains)) {
+    error_log(sprintf("Skipped domain %s", $parsed_mail['domain']));
+    continue;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `goto` FROM `alias`
+      WHERE
+      (
+        `address` = :rcpt
+        OR
+        `address` IN (
+          SELECT username FROM mailbox, alias_domain
+            WHERE (alias_domain.alias_domain = :domain_part
+              AND mailbox.username = CONCAT(:local_part, '@', alias_domain.target_domain)
+              AND mailbox.active = '1'
+              AND alias_domain.active='1')
+        )
+      )
+      AND `active`= '1';");
+    $stmt->execute(array(
+      ':rcpt' => $rcpt,
+      ':local_part' => $parsed_mail['local'],
+      ':domain_part' => $parsed_mail['domain']
+    ));
+    $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
+    if (!empty($gotos)) {
+      $filtered_rcpts  = array_unique(array_merge($filtered_rcpts, explode(',', $gotos)));
+    }
+  }
+  catch (PDOException $e) {
+    error_log($e->getMessage());
+    http_response_code(502);
+    exit;
+  }
+}
+foreach ($filtered_rcpts as $rcpt) {
+  
+  try {
+    $stmt = $pdo->prepare("INSERT INTO `quarantaine` (`qid`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`)
+      VALUES (:qid, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action)");
+    $stmt->execute(array(
+      ':qid' => $qid,
+      ':score' => $score,
+      ':sender' => $sender,
+      ':rcpt' => $rcpt,
+      ':symbols' => $symbols,
+      ':user' => $user,
+      ':ip' => $ip,
+      ':msg' => $raw_data,
+      ':action' => $action
+    ));
+    $stmt = $pdo->prepare('DELETE FROM `quarantaine` WHERE `id` NOT IN ( 
+      SELECT `id`
+      FROM (
+        SELECT `id`
+        FROM `quarantaine`
+        WHERE `rcpt` = :rcpt
+        ORDER BY id DESC
+        LIMIT :retention_size
+      ) x 
+    );');
+    $stmt->execute(array(
+      ':rcpt' => $rcpt,
+      ':retention_size' => $retention_size
+    ));
+  }
+  catch (PDOException $e) {
+    error_log($e->getMessage());
+    http_response_code(503);
+    exit;
+  }
+}
+

+ 3 - 0
data/conf/rspamd/meta_exporter/vars.inc.php

@@ -0,0 +1,3 @@
+<?php
+require_once('../../../web/inc/vars.inc.php');
+?>

+ 1 - 0
data/conf/rspamd/override.d/logging.inc

@@ -1,3 +1,4 @@
 type = "console";
 systemd = false;
 .include "$CONFDIR/logging.inc"
+.include(try=true; priority=20) "$CONFDIR/override.d/logging.custom.inc"

+ 1 - 1
data/conf/rspamd/override.d/ratelimit.conf

@@ -9,6 +9,6 @@ rates {
 }
 whitelisted_rcpts = "postmaster,mailer-daemon";
 max_rcpt = 5;
-custom_keywords = "/etc/rspamd/custom/ratelimit.lua";
+custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
 user_keywords = ["user", "customrl"];
 dynamic_rates = { customrl = "customrl"}

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

@@ -0,0 +1 @@
+# Placeholder

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

@@ -1,8 +1,9 @@
 bind_socket = "*:11334";
-enable_password = "$2$pppq86q9uns51zd5ekfxecj7bxwaefo3$p7f9xdhamydjhtypcr639it3kqeiknx3dk9on7skjypyi8uwwcmy";
 secure_ip = "192.168.0.0/16";
 secure_ip = "172.16.0.0/12";
 secure_ip = "10.0.0.0/8";
 secure_ip = "127.0.0.1";
 secure_ip = "::1";
 secure_ip = "fd4d:6169:6c63:6f77::/64"
+.include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc"
+.include(try=true; priority=20) "$CONFDIR/override.d/worker-controller.custom.inc" 

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

@@ -1,2 +1,3 @@
 bind_socket = "*:11333";
 task_timeout = 12s;
+.include(try=true; priority=20) "$CONFDIR/override.d/worker-normal.custom.inc"

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

@@ -5,3 +5,4 @@ upstream {
   default = true;
   hosts = "rspamd:11333"
 }
+.include(try=true; priority=20) "$CONFDIR/override.d/worker-proxy.custom.inc"

+ 104 - 131
data/web/admin.php

@@ -7,26 +7,9 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $tfa_data = get_tfa();
 ?>
 <div class="container">
-
   <ul class="nav nav-tabs" role="tablist">
-    <li role="presentation" class="active">
-      <a href="#tab-access" aria-controls="tab-access" role="tab" data-toggle="tab"><?=$lang['admin']['access'];?></a>
-    </li>
-    <li role="presentation">
-      <a href="#tab-config" aria-controls="tab-config" role="tab" data-toggle="tab"><?=$lang['admin']['configuration'];?></a>
-    </li>
-    <li class="dropdown">
-    <a class="dropdown-toggle" data-toggle="dropdown" href="#">Logs
-    <span class="caret"></span></a>
-    <ul class="dropdown-menu">
-    <li role="presentation"><a href="#tab-postfix-logs" aria-controls="tab-postfix-logs" role="tab" data-toggle="tab">Postfix</a></li>
-    <li role="presentation"><a href="#tab-dovecot-logs" aria-controls="tab-dovecot-logs" role="tab" data-toggle="tab">Dovecot</a></li>
-    <li role="presentation"><a href="#tab-sogo-logs" aria-controls="tab-sogo-logs" role="tab" data-toggle="tab">SOGo</a></li>
-    <li role="presentation"><a href="#tab-fail2ban-logs" aria-controls="tab-fail2ban-logs" role="tab" data-toggle="tab">Fail2ban</a></li>
-    <li role="presentation"><a href="#tab-rspamd-history" aria-controls="tab-rspamd-history" role="tab" data-toggle="tab">Rspamd</a></li>
-    <li role="presentation"><a href="#tab-autodiscover-logs" aria-controls="tab-autodiscover-logs" role="tab" data-toggle="tab">Autodiscover</a></li>
-    </ul>
-    </li>
+    <li role="presentation" class="active"><a href="#tab-access" aria-controls="tab-access" role="tab" data-toggle="tab"><?=$lang['admin']['access'];?></a></li>
+    <li role="presentation"><a href="#tab-config" aria-controls="tab-config" role="tab" data-toggle="tab"><?=$lang['admin']['configuration'];?></a></li>
   </ul>
 
   <div class="tab-content" style="padding-top:20px">
@@ -57,7 +40,7 @@ $tfa_data = get_tfa();
           </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="null" data-api-url='edit/self' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+              <button class="btn btn-default" id="edit_selected" data-id="admin" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
             </div>
           </div>
         </form>
@@ -95,6 +78,42 @@ $tfa_data = get_tfa();
         </div>
       </div>
     </div>
+
+    <div class="hidden panel panel-primary">
+      <div class="panel-heading">API</div>
+      <div class="panel-body">
+        <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>
+            <div class="col-sm-9">
+              <textarea class="form-control" rows="5" name="allow_from" id="allow_from" required><?=htmlspecialchars($admindetails['allow_from']);?></textarea>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="admin_api_key"><?=$lang['admin']['api_key'];?>:</label>
+            <div class="col-sm-9">
+              <input type="text" class="form-control" placeholder="-" value="<?=htmlspecialchars($admindetails['api_key']);?>" readonly>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <label>
+                <input type="checkbox" name="active" <?=($admindetails['api_active'] == 1) ? 'checked' : null;?>> <?=$lang['admin']['activate_api'];?>
+              </label>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <div class="btn-group">
+                <button class="btn btn-default" name="admin_api" type="submit" href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+                <button class="btn btn-info" name="admin_api_regen_key" type="submit" href="#"><?=$lang['admin']['regen_api_key'];?></button>
+              </div>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+
     <div class="panel panel-default">
     <div class="panel-heading"><?=$lang['admin']['domain_admins'];?></div>
         <div class="panel-body">
@@ -120,7 +139,6 @@ $tfa_data = get_tfa();
     </div>
   </div>
 
-
   <div role="tabpanel" class="tab-pane" id="tab-config">
     <div class="row">
     <div id="sidebar-admin" class="col-sm-2 hidden-xs">
@@ -129,6 +147,7 @@ $tfa_data = get_tfa();
         <a href="#fwdhosts" class="list-group-item"><?=$lang['admin']['forwarding_hosts'];?></a>
         <a href="#f2bparams" class="list-group-item"><?=$lang['admin']['f2b_parameters'];?></a>
         <a href="#relayhosts" class="list-group-item">Relayhosts</a>
+        <a href="#quarantaine" class="list-group-item">Quarantaine</a>
         <a href="#customize" class="list-group-item"><?=$lang['admin']['customize'];?></a>
         <a href="#top" class="list-group-item" style="border-top:1px dashed #dadada">↸ <?=$lang['admin']['to_top'];?></a>
       </div>
@@ -230,7 +249,7 @@ $tfa_data = get_tfa();
         ?>
 
         <legend style="margin-top:40px"><?=$lang['admin']['dkim_add_key'];?></legend>
-        <form class="form-inline" data-id="dkim" role="form" method="post">
+        <form class="form" data-id="dkim" role="form" method="post">
           <div class="form-group">
             <label for="domain">Domain</label>
             <input class="form-control" id="domain" name="domain" placeholder="example.org" required>
@@ -291,7 +310,7 @@ $tfa_data = get_tfa();
         </div>
         <legend><?=$lang['admin']['add_forwarding_host'];?></legend>
         <p class="help-block"><?=$lang['admin']['forwarding_hosts_add_hint'];?></p>
-        <form class="form-inline" data-id="fwdhost" role="form" method="post">
+        <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>
@@ -328,7 +347,7 @@ $tfa_data = get_tfa();
             <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
           </div>
           <div class="form-group">
-            <label for="retry_window"><?=$lang['admin']['f2b_whitelist'];?>:</label>
+            <label for="whitelist"><?=$lang['admin']['f2b_whitelist'];?>:</label>
             <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
           </div>
           <button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
@@ -358,7 +377,7 @@ $tfa_data = get_tfa();
         </div>
         <legend><?=$lang['admin']['add_relayhost'];?></legend>
         <p class="help-block"><?=$lang['admin']['add_relayhost_add_hint'];?></p>
-        <form class="form-inline" data-id="rlyhost" role="form" method="post">
+        <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>
@@ -376,6 +395,43 @@ $tfa_data = get_tfa();
       </div>
     </div>
 
+    <span class="anchor" id="quarantaine"></span>
+    <div class="panel panel-default">
+      <div class="panel-heading">Quarantäne</div>
+      <div class="panel-body">
+       <?php $q_data = quarantaine('settings'); ?>
+        <form class="form" data-id="quarantaine" role="form" method="post">
+          <div class="row">
+            <div class="col-sm-6">
+              <div class="form-group">
+                <label for="retention_size">Rückhaltungen pro Mailbox:</label>
+                <input type="number" class="form-control" id="retention_size" name="retention_size" value="<?=$q_data['retention_size'];?>" required>
+              </div>
+            </div>
+            <div class="col-sm-6">
+              <div class="form-group">
+                <label for="max_size">Maximale Größe in MiB (größere Elemente werden verworfen):</label>
+                <input type="number" class="form-control" id="max_size" name="max_size" value="<?=$q_data['max_size'];?>" required>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <label for="exclude_domains">Domains und Alias-Domains ausschließen:</label><br />
+            <select data-width="100%" id="exclude_domains" name="exclude_domains" class="selectpicker" title="<?=$lang['tfa']['select'];?>" multiple>
+              <?php
+              foreach (array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')) as $domain):
+              ?>
+                <option <?=(in_array($domain, $q_data['exclude_domains'])) ? 'selected' : null;?>><?=htmlspecialchars($domain);?></option>
+              <?php
+              endforeach;
+              ?>
+            </select>
+          </div>
+          <button class="btn btn-success" id="edit_selected" data-item="self" data-id="quarantaine" data-api-url='edit/quarantaine' data-api-attr='{"action":"settings"}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+        </form>
+      </div>
+    </div>
+
     <span class="anchor" id="customize"></span>
     <div class="panel panel-default">
       <div class="panel-heading"><?=$lang['admin']['customize'];?></div>
@@ -444,10 +500,29 @@ $tfa_data = get_tfa();
             endforeach;
             ?>
           </table>
-          <div class="btn-group">
-            <button class="btn btn-success" id="edit_selected" data-item="admin" data-id="app_links" data-reload="no" data-api-url='edit/app_links' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
-            <button class="btn btn-default" type="button" id="add_app_link_row"><?=$lang['admin']['add_row'];?></button>
-          </div> 
+          <p><div class="btn-group">
+            <button class="btn btn-sm btn-success" id="edit_selected" data-item="admin" data-id="app_links" data-reload="no" data-api-url='edit/app_links' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+            <button class="btn btn-sm btn-default" type="button" id="add_app_link_row"><?=$lang['admin']['add_row'];?></button>
+          </div></p>
+        </form>
+        <legend><?=$lang['admin']['ui_texts'];?></legend>
+        <?php
+        $ui_texts = customize('get', 'ui_texts');
+        ?>
+        <form class="form" data-id="uitexts" role="form" method="post">
+          <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'];?>">
+          </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'];?>">
+          </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-success" id="edit_selected" data-item="null" 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>
@@ -455,108 +530,6 @@ $tfa_data = get_tfa();
   </div>
   </div>
 
-  <div role="tabpanel" class="tab-pane" id="tab-postfix-logs">
-    <div class="panel panel-default">
-      <div class="panel-heading">Postfix <span class="badge badge-info log-lines"></span>
-        <div class="btn-group pull-right">
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="100">+ 100</button>
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="1000">+ 1000</button>
-          <button class="btn btn-xs btn-default" id="refresh_postfix_log"><?=$lang['admin']['refresh'];?></button>
-        </div>
-      </div>
-      <div class="panel-body">
-        <div class="table-responsive">
-          <table class="table table-striped table-condensed" id="postfix_log"></table>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
-    <div class="panel panel-default">
-      <div class="panel-heading">Dovecot <span class="badge badge-info log-lines"></span>
-        <div class="btn-group pull-right">
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="100">+ 100</button>
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="1000">+ 1000</button>
-          <button class="btn btn-xs btn-default" id="refresh_dovecot_log"><?=$lang['admin']['refresh'];?></button>
-        </div>
-      </div>
-      <div class="panel-body">
-        <div class="table-responsive">
-          <table class="table table-striped table-condensed" id="dovecot_log"></table>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div role="tabpanel" class="tab-pane" id="tab-sogo-logs">
-    <div class="panel panel-default">
-      <div class="panel-heading">SOGo <span class="badge badge-info log-lines"></span>
-        <div class="btn-group pull-right">
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="100">+ 100</button>
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="1000">+ 1000</button>
-          <button class="btn btn-xs btn-default" id="refresh_sogo_log"><?=$lang['admin']['refresh'];?></button>
-        </div>
-      </div>
-      <div class="panel-body">
-        <div class="table-responsive">
-          <table class="table table-striped table-condensed" id="sogo_log"></table>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div role="tabpanel" class="tab-pane" id="tab-fail2ban-logs">
-    <div class="panel panel-default">
-      <div class="panel-heading">Fail2ban <span class="badge badge-info log-lines"></span>
-        <div class="btn-group pull-right">
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="100">+ 100</button>
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="1000">+ 1000</button>
-          <button class="btn btn-xs btn-default" id="refresh_fail2ban_log"><?=$lang['admin']['refresh'];?></button>
-        </div>
-      </div>
-      <div class="panel-body">
-        <div class="table-responsive">
-          <table class="table table-striped table-condensed" id="fail2ban_log"></table>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div role="tabpanel" class="tab-pane" id="tab-rspamd-history">
-    <div class="panel panel-default">
-      <div class="panel-heading">Rspamd history <span class="badge badge-info log-lines"></span>
-        <div class="btn-group pull-right">
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="100">+ 100</button>
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="1000">+ 1000</button>
-          <button class="btn btn-xs btn-default" id="refresh_rspamd_history"><?=$lang['admin']['refresh'];?></button>
-        </div>
-      </div>
-      <div class="panel-body">
-        <div class="table-responsive">
-          <table class="table table-striped table-condensed log-table" id="rspamd_history"></table>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div role="tabpanel" class="tab-pane" id="tab-autodiscover-logs">
-    <div class="panel panel-default">
-      <div class="panel-heading">Autodiscover <span class="badge badge-info log-lines"></span>
-        <div class="btn-group pull-right">
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="100">+ 100</button>
-          <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="1000">+ 1000</button>
-          <button class="btn btn-xs btn-default" id="refresh_autodiscover_log"><?=$lang['admin']['refresh'];?></button>
-        </div>
-      </div>
-      <div class="panel-body">
-        <div class="table-responsive">
-          <table class="table table-striped table-condensed" id="autodiscover_log"></table>
-        </div>
-      </div>
-    </div>
-  </div>
-
   </div>
 </div> <!-- /container -->
 <?php

+ 1 - 0
data/web/css/breakpoint.min.css

@@ -0,0 +1 @@
+@media (max-width:1050px){.navbar-header{float:none}.navbar-left,.navbar-nav,.navbar-right{float:none!important}.navbar-toggle{display:block}.navbar-collapse{border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-collapse.collapse{display:none!important}.navbar-nav{margin-top:7.5px}.navbar-nav>li{float:none}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px}.collapse.in{display:block!important}.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}}

+ 37 - 0
data/web/css/debug.css

@@ -0,0 +1,37 @@
+table.footable>tbody>tr.footable-empty>td {
+  font-size:15px !important;
+  font-style:italic;
+}
+.pagination a {
+  text-decoration: none !important;
+}
+.panel panel-default {
+  overflow: visible !important;
+}
+.table-responsive {
+  overflow: visible !important;
+}
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    overflow-x: scroll !important;
+  }
+}
+.footer-add-item {
+  display:block;
+  text-align: center;
+  font-style: italic;
+  padding: 10px;
+  background: #F5F5F5;
+}
+@media (min-width: 992px) {
+  .container {
+      width: 80%;
+  }
+}
+.mass-actions-debug {
+  user-select: none;
+  padding:10px 0 10px 10px;
+}
+.inputMissingAttr {
+  border-color: #FF4136;
+}

+ 9 - 1
data/web/css/mailbox.css

@@ -34,4 +34,12 @@ table.footable>tbody>tr.footable-empty>td {
 }
 .inputMissingAttr {
   border-color: #FF4136;
-}
+}
+.dns-found {
+  max-width: 300px;
+  word-break: break-all;
+}
+.dns-recommended {
+  max-width: 150px;
+  word-break: break-all;
+}

+ 37 - 0
data/web/css/quarantaine.css

@@ -0,0 +1,37 @@
+table.footable>tbody>tr.footable-empty>td {
+  font-size:15px !important;
+  font-style:italic;
+}
+.pagination a {
+  text-decoration: none !important;
+}
+.panel panel-default {
+  overflow: visible !important;
+}
+.table-responsive {
+  overflow: visible !important;
+}
+@media screen and (max-width: 767px) {
+  .table-responsive {
+    overflow-x: scroll !important;
+  }
+}
+.footer-add-item {
+  display:block;
+  text-align: center;
+  font-style: italic;
+  padding: 10px;
+  background: #F5F5F5;
+}
+@media (min-width: 992px) {
+  .container {
+      width: 80%;
+  }
+}
+.mass-actions-quarantaine {
+  user-select: none;
+  padding:10px 0 10px 10px;
+}
+.inputMissingAttr {
+  border-color: #FF4136;
+}

+ 328 - 0
data/web/debug.php

@@ -0,0 +1,328 @@
+<?php
+require_once "inc/prerequisites.inc.php";
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
+require_once "inc/header.inc.php";
+$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+
+?>
+<div class="container">
+
+  <ul class="nav nav-tabs" role="tablist">
+    <li class="dropdown active"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Rspamd
+      <span class="caret"></span></a>
+      <ul class="dropdown-menu">
+        <li role="presentation" class="active"><a href="#tab-rspamd-ui" aria-controls="tab-rspamd-ui" role="tab" data-toggle="tab">Rspamd UI</a></li>
+        <li role="presentation"><a href="#tab-rspamd-settings" aria-controls="tab-rspamd-settings" role="tab" data-toggle="tab">Rspamd settings map</a></li>
+      </ul>
+    </li>
+    <li role="presentation"><a href="#tab-containers" aria-controls="tab-containers" role="tab" data-toggle="tab">Containers</a></li>
+    <li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Logs
+      <span class="caret"></span></a>
+      <ul class="dropdown-menu">
+        <li role="presentation"><a href="#tab-postfix-logs" aria-controls="tab-postfix-logs" role="tab" data-toggle="tab">Postfix</a></li>
+        <li role="presentation"><a href="#tab-dovecot-logs" aria-controls="tab-dovecot-logs" role="tab" data-toggle="tab">Dovecot</a></li>
+        <li role="presentation"><a href="#tab-sogo-logs" aria-controls="tab-sogo-logs" role="tab" data-toggle="tab">SOGo</a></li>
+        <li role="presentation"><a href="#tab-fail2ban-logs" aria-controls="tab-fail2ban-logs" role="tab" data-toggle="tab">Fail2ban</a></li>
+        <li role="presentation"><a href="#tab-rspamd-history" aria-controls="tab-rspamd-history" role="tab" data-toggle="tab">Rspamd</a></li>
+        <li role="presentation"><a href="#tab-autodiscover-logs" aria-controls="tab-autodiscover-logs" role="tab" data-toggle="tab">Autodiscover</a></li>
+        <li role="presentation"><a href="#tab-watchdog-logs" aria-controls="tab-watchdog-logs" role="tab" data-toggle="tab">Watchdog</a></li>
+        <li role="presentation"><a href="#tab-acme-logs" aria-controls="tab-acme-logs" role="tab" data-toggle="tab">ACME</a></li>
+        <li role="presentation"><a href="#tab-api-logs" aria-controls="tab-api-logs" role="tab" data-toggle="tab">API</a></li>
+      </ul>
+    </li>
+  </ul>
+
+	<div class="row">
+		<div class="col-md-12">
+      <div class="tab-content" style="padding-top:20px">
+
+        <div role="tabpanel" class="tab-pane active" id="tab-rspamd-ui">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h3 class="panel-title">Rspamd UI</h3>
+            </div>
+            <div class="panel-body">
+              <div class="row">
+                <div class="col-sm-9">
+                <form class="form-horizontal" autocapitalize="none" data-id="admin" autocorrect="off" role="form" method="post">
+                  <div class="form-group">
+                    <div class="col-sm-offset-3 col-sm-9">
+                      <label>
+                        <a href="/rspamd/" target="_blank"><span class="glyphicon glyphicon-new-window" aria-hidden="true"></span> Rspamd UI</a>
+                      </label>
+                    </div>
+                  </div>
+                  <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">
+                    </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">
+                    </div>
+                  </div>
+                  <div class="form-group">
+                    <div class="col-sm-offset-3 col-sm-9">
+                      <button type="submit" class="btn btn-default" id="rspamd_ui" name="rspamd_ui" href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+                    </div>
+                  </div>
+                </form>
+                </div>
+                <div class="col-sm-3">
+                  <img class="img-responsive" src="/img/rspamd_logo.png" alt="Rspamd UI" />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-rspamd-settings">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h3 class="panel-title">Rspamd settings map</h3>
+            </div>
+            <div class="panel-body">
+            <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control" rows="20" id="settings_map" name="settings_map" readonly><?=file_get_contents('http://nginx:8081/settings.php');?></textarea>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-containers">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h3 class="panel-title">Container information</h3>
+            </div>
+            <div class="panel-body">
+            <ul class="list-group">
+            <?php
+            $container_array = array(
+              'nginx-mailcow',
+              'rspamd-mailcow',
+              'postfix-mailcow',
+              'dovecot-mailcow',
+              'sogo-mailcow',
+              'acme-mailcow',
+              'memcached-mailcow',
+              'watchdog-mailcow',
+              'unbound-mailcow',
+              'redis-mailcow',
+              'php-fpm-mailcow',
+              'mysql-mailcow',
+              'fail2ban-mailcow',
+              'clamd-mailcow'
+            );
+            foreach ($container_array as $container) {
+                $container_stats = docker($container, 'info');
+                ?>
+                <li class="list-group-item">
+                <?=$container;?>
+                <?php
+                date_default_timezone_set('UTC');
+                $StartedAt = date_parse($container_stats['State']['StartedAt']);
+                $date = new \DateTime();
+                $date->setTimestamp(mktime(
+                  $StartedAt['hour'],
+                  $StartedAt['minute'],
+                  $StartedAt['second'],
+                  $StartedAt['month'],
+                  $StartedAt['day'],
+                  $StartedAt['year']));
+                $user_tz = new DateTimeZone(getenv('TZ'));
+                $date->setTimezone($user_tz);
+                $started = $date->format('r');
+                ?>
+                <small>(Started on <?=$started;?>),
+                <a href data-toggle="modal" data-container="<?=$container;?>" data-target="#RestartContainer">Restart</a></small>
+                <span class="pull-right label label-<?=($container_stats['State']['Running'] == 1) ? 'success' : 'danger';?>">&nbsp;&nbsp;&nbsp;</span>
+                </li>
+              <?php
+              }
+            ?>
+            </ul>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-postfix-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">Postfix <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_postfix_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="postfix_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">Dovecot <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_dovecot_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="dovecot_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-sogo-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">SOGo <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_sogo_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="sogo_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-fail2ban-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">Fail2ban <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_fail2ban_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="fail2ban_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-rspamd-history">
+          <div class="panel panel-default">
+            <div class="panel-heading">Rspamd history <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_rspamd_history"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed log-table" id="rspamd_history"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-autodiscover-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">Autodiscover <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_autodiscover_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="autodiscover_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-watchdog-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">Watchdog <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="watchdog" data-table="watchdog_log" data-log-url="watchdog" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="watchdog" data-table="watchdog_log" data-log-url="watchdog" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_watchdog_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="watchdog_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-acme-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">ACME <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="acme_log" data-log-url="acme" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="acme_log" data-log-url="acme" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_acme_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="acme_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-api-logs">
+          <div class="panel panel-default">
+            <div class="panel-heading">API <span class="badge badge-info log-lines"></span>
+              <div class="btn-group pull-right">
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="apilog" data-table="api_log" data-log-url="api" data-nrows="100">+ 100</button>
+                <button class="btn btn-xs btn-default add_log_lines" data-post-process="apilog" data-table="api_log" data-log-url="api" data-nrows="1000">+ 1000</button>
+                <button class="btn btn-xs btn-default" id="refresh_api_log"><?=$lang['admin']['refresh'];?></button>
+              </div>
+            </div>
+            <div class="panel-body">
+              <div class="table-responsive">
+                <table class="table table-striped table-condensed" id="api_log"></table>
+              </div>
+            </div>
+          </div>
+        </div>
+
+      </div> <!-- /tab-content -->
+    </div> <!-- /col-md-12 -->
+  </div> <!-- /row -->
+</div> <!-- /container -->
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/debug.php';
+?>
+<script type='text/javascript'>
+<?php
+$lang_admin = json_encode($lang['admin']);
+echo "var lang = ". $lang_admin . ";\n";
+echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
+echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n";
+
+?>
+</script>
+<script src="js/footable.min.js"></script>
+<script src="js/debug.js"></script>
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
+}
+else {
+	header('Location: /');
+	exit();
+}
+?>

+ 0 - 241
data/web/diagnostics.php

@@ -1,241 +0,0 @@
-<?php
-require_once 'inc/prerequisites.inc.php';
-require_once 'inc/spf.inc.php';
-
-define('state_good',  "&#10003;");
-define('state_missing',   "&#x2717;");
-define('state_nomatch', "?");
-define('state_optional', "(optional)");
-
-if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
-require_once("inc/header.inc.php");
-
-$ch = curl_init('http://ip4.mailcow.email');
-curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
-curl_setopt($ch, CURLOPT_VERBOSE, false);
-curl_setopt($ch, CURLOPT_HEADER, false);
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
-$ip = curl_exec($ch);
-curl_close($ch);
-
-$ch = curl_init('http://ip6.mailcow.email');
-curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
-curl_setopt($ch, CURLOPT_VERBOSE, false);
-curl_setopt($ch, CURLOPT_HEADER, false);
-curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
-$ip6 = curl_exec($ch);
-curl_close($ch);
-
-$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
-if (!empty($ip6)) {
-  $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6);
-  $ip6_full = str_replace('::', ':0:', $ip6_full);
-  $ip6_full = str_replace('::', ':0:', $ip6_full);
-  $ptr6 = '';
-  foreach (explode(':', $ip6_full) as $part) {
-    $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT);
-  }
-  $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa';
-}
-
-$https_port = strpos($_SERVER['HTTP_HOST'], ':');
-if ($https_port === FALSE) {
-  $https_port = 443;
-} else {
-  $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1);
-}
-
-$records = array();
-$records[] = array($mailcow_hostname, 'A', $ip);
-$records[] = array($ptr, 'PTR', $mailcow_hostname);
-if (!empty($ip6)) {
-  $records[] = array($mailcow_hostname, 'AAAA', $ip6);
-  $records[] = array($ptr6, 'PTR', $mailcow_hostname);
-}
-$domains = mailbox('get', 'domains');
-foreach(mailbox('get', 'domains') as $domain) {
-  $domains = array_merge($domains, mailbox('get', 'alias_domains', $domain));
-}
-
-if (!isset($autodiscover_config['sieve'])) {
-  $autodiscover_config['sieve'] = array('server' => $mailcow_hostname, 'port' => array_pop(explode(':', getenv('SIEVE_PORT'))));
-}
-
-$records[] = array('_25._tcp.'                                              . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1));
-$records[] = array('_' . $https_port    . '._tcp.' . $mailcow_hostname, 'TLSA', generate_tlsa_digest($mailcow_hostname, $https_port));
-$records[] = array('_' . $autodiscover_config['pop3']['tlsport'] . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['tlsport'], 1));
-$records[] = array('_' . $autodiscover_config['imap']['tlsport'] . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['tlsport'], 1));
-$records[] = array('_' . $autodiscover_config['smtp']['port']    . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['port']));
-$records[] = array('_' . $autodiscover_config['smtp']['tlsport'] . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['tlsport'], 1));
-$records[] = array('_' . $autodiscover_config['imap']['port']    . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['port']));
-$records[] = array('_' . $autodiscover_config['pop3']['port']    . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['port']));
-$records[] = array('_' . $autodiscover_config['sieve']['port']   . '._tcp.' . $autodiscover_config['sieve']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['sieve']['server'], $autodiscover_config['sieve']['port'], 1));
-
-foreach ($domains as $domain) {
-  $records[] = array($domain, 'MX', $mailcow_hostname);
-  $records[] = array('autodiscover.' . $domain, 'CNAME', $mailcow_hostname);
-  $records[] = array('_autodiscover._tcp.' . $domain, 'SRV', $mailcow_hostname . ' ' . $https_port);
-  $records[] = array('autoconfig.' . $domain, 'CNAME', $mailcow_hostname);
-  $records[] = array($domain, 'TXT', '<a href="http://www.openspf.org/SPF_Record_Syntax" target="_blank">SPF Record Syntax</a>', state_optional);
-  $records[] = array('_dmarc.' . $domain, 'TXT', '<a href="http://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>', state_optional);
-  
-  if (!empty($dkim = dkim('details', $domain))) {
-    $records[] = array($dkim['dkim_selector'] . '._domainkey.' . $domain, 'TXT', $dkim['dkim_txt']);
-  }
-
-  $current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV);
-  if (count($current_records) == 0 || $current_records[0]['target'] != '') {
-    if ($autodiscover_config['pop3']['tlsport'] != '110')  {
-      $records[] = array('_pop3._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']);
-    }
-  } else {
-      $records[] = array('_pop3._tcp.' . $domain, 'SRV', '. 0');
-  }
-  $current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV);
-  if (count($current_records) == 0 || $current_records[0]['target'] != '') {
-    if ($autodiscover_config['pop3']['port'] != '995')  {
-      $records[] = array('_pop3s._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']);
-    }
-  } else {
-      $records[] = array('_pop3s._tcp.' . $domain, 'SRV', '. 0');
-  }
-  if ($autodiscover_config['imap']['tlsport'] != '143')  {
-    $records[] = array('_imap._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']);
-  }
-  if ($autodiscover_config['imap']['port'] != '993')  {
-    $records[] = array('_imaps._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']);
-  }
-  if ($autodiscover_config['smtp']['tlsport'] != '587')  {
-    $records[] = array('_submission._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']);
-  }
-  if ($autodiscover_config['smtp']['port'] != '465')  {
-    $records[] = array('_smtps._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']);
-  }
-  if ($autodiscover_config['sieve']['port'] != '4190')  {
-    $records[] = array('_sieve._tcp.' . $domain, 'SRV', $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']);
-  }
-}
-
-$record_types = array(
-  'A' => DNS_A,
-  'AAAA' => DNS_AAAA,
-  'CNAME' => DNS_CNAME,
-  'MX' => DNS_MX,
-  'PTR' => DNS_PTR,
-  'SRV' => DNS_SRV,
-  'TXT' => DNS_TXT,
-);
-$data_field = array(
-  'A' => 'ip',
-  'AAAA' => 'ipv6',
-  'CNAME' => 'target',
-  'MX' => 'target',
-  'PTR' => 'target',
-  'SRV' => 'data',
-  'TLSA' => 'data',
-  'TXT' => 'txt',
-);
-?>
-<div class="container">
-  <h3><?=$lang['diagnostics']['dns_records'];?></h3>
-  <p><?=$lang['diagnostics']['dns_records_24hours'];?></p>
-  <div class="table-responsive" id="dnstable">
-    <table class="table table-striped">
-      <tr> <th><?=$lang['diagnostics']['dns_records_name'];?></th> <th><?=$lang['diagnostics']['dns_records_type'];?></th> <th><?=$lang['diagnostics']['dns_records_data'];?></th ><th><?=$lang['diagnostics']['dns_records_status'];?></th> </tr>
-<?php
-foreach ($records as $record)
-{
-  $record[1] = strtoupper($record[1]);
-  $state = state_missing;
-  if ($record[1] == 'TLSA') {
-    $currents = dns_get_record($record[0], 52, $_, $_, TRUE);
-    foreach ($currents as &$current) {
-      $current['type'] = 'TLSA';
-      $current['cert_usage'] = hexdec(bin2hex($current['data']{0}));
-      $current['selector'] = hexdec(bin2hex($current['data']{1}));
-      $current['match_type'] = hexdec(bin2hex($current['data']{2}));
-      $current['cert_data'] = bin2hex(substr($current['data'], 3));
-      $current['data'] = $current['cert_usage'] . ' ' . $current['selector'] . ' ' . $current['match_type'] . ' ' . $current['cert_data'];
-    }
-    unset($current);
-  }
-  else {
-    $currents = dns_get_record($record[0], $record_types[$record[1]]);
-    if ($record[1] == 'SRV') {
-      foreach ($currents as &$current) {
-        if ($current['target'] == '') {
-          $current['target'] = '.';
-          $current['port'] = '0';
-        }
-        $current['data'] = $current['target'] . ' ' . $current['port'];
-      }
-      unset($current);
-    }
-  }
-  
-  if ($record[1] == 'CNAME' && count($currents) == 0) {
-    // A and AAAA are also valid instead of CNAME
-    $a = dns_get_record($record[0], DNS_A);
-    $cname = dns_get_record($record[2], DNS_A);
-    if (count($a) > 0 && count($cname) > 0) {
-      if ($a[0]['ip'] == $cname[0]['ip']) {
-        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $record[2]));
-      
-        $aaaa = dns_get_record($record[0], DNS_AAAA);
-        $cname = dns_get_record($record[2], DNS_AAAA);
-        if (count($aaaa) == 0 || count($cname) == 0 || $aaaa[0]['ipv6'] != $cname[0]['ipv6']) {
-          $currents[0]['target'] = $aaaa[0]['ipv6'];
-        }
-      } else {
-        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $a[0]['ip']));
-      }
-    }
-  }
-  
-  foreach ($currents as $current) {
-    $current['type'] == strtoupper($current['type']);
-    if ($current['type'] != $record[1])
-    {
-      continue;
-    }
-    
-    elseif ($current['type'] == 'TXT' && strpos($record[0], '_dmarc.') === 0) {
-      $state = state_optional . '<br />' . $current[$data_field[$current['type']]];
-    }
-    else if ($current['type'] == 'TXT' && strpos($current['txt'], 'v=spf1') === 0) {
-      $state = state_optional . '<br />' . $current[$data_field[$current['type']]];
-    }
-    else if ($current['type'] != 'TXT' && isset($data_field[$current['type']]) && $state != state_good) {
-      $state = state_nomatch;
-      if ($current[$data_field[$current['type']]] == $record[2])
-        $state = state_good;
-    }
-  }
-  
-  if (isset($record[3]) && $record[3] == state_optional && ($state == state_missing || $state == state_nomatch)) {
-    $state = state_optional;
-  }
-  
-  if ($state == state_nomatch) {
-    $state = array();
-    foreach ($currents as $current) {
-      $state[] = $current[$data_field[$current['type']]];
-    }
-    $state = implode('<br />', $state);
-  }
-
-  echo sprintf('<tr><td>%s</td><td>%s</td><td style="max-width: 300px; word-break: break-all">%s</td><td style="max-width: 150px; word-break: break-all">%s</td></tr>', $record[0], $record[1], $record[2], $state);
-}
-?>
-    </table>
-	</div>
-</div>
-<?php
-require_once("inc/footer.inc.php");
-} else {
-  header('Location: index.php');
-  exit();
-}
-?>

+ 48 - 4
data/web/edit.php

@@ -608,6 +608,52 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         <?php
         }
     }
+    elseif (isset($_GET['bcc']) && !empty($_GET["bcc"])) {
+        $bcc = intval($_GET["bcc"]);
+        $result = bcc('details', $bcc);
+        if (!empty($result)) {
+          ?>
+          <h4>BCC map</h4>
+          <br />
+          <form class="form-horizontal" data-id="editbcc" role="form" method="post">
+            <input type="hidden" value="0" name="active">
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="bcc_dest">BCC destination</label>
+              <div class="col-sm-10">
+                <textarea id="bcc_dest" class="form-control" autocapitalize="none" autocorrect="off" rows="10" id="bcc_dest" name="bcc_dest" required><?=$result['bcc_dest'];?></textarea>
+                <small>BCC destinations can only be valid email addresses. Separated by whitespace, semicolon, new line or comma.</small>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="type">Type:</label>
+              <div class="col-sm-10">
+                <select id="addFilterType" name="type" id="type" required>
+                  <option value="sender" <?=($result['type'] == 'sender') ? 'selected' : null;?>>Sender map</option>
+                  <option value="rcpt" <?=($result['type'] == 'rcpt') ? 'selected' : null;?>>Recipient map</option>
+                </select>
+              </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" id="edit_selected" data-id="editbcc" data-item="<?=$bcc;?>" data-api-url='edit/bcc' 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
+        }
+    }
   }
   if ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin" || $_SESSION['mailcow_cc_role'] == "user") {
     if (isset($_GET['syncjob']) &&
@@ -722,9 +768,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         <?php
         }
       }
-  }
-  if ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin" || $_SESSION['mailcow_cc_role'] == "user") {
-    if (isset($_GET['filter']) &&
+    elseif (isset($_GET['filter']) &&
       is_numeric($_GET['filter'])) {
         $id = $_GET["filter"];
         $result = mailbox('get', 'filter_details', $id);
@@ -774,7 +818,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
         <?php
         }
-      }
+    }
   }
 }
 else {

BIN
data/web/img/rspamd_logo.png


+ 53 - 0
data/web/inc/ajax/container_ctrl.php

@@ -0,0 +1,53 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
+	exit();
+}
+
+if (preg_match('/^[a-z\-]{0,}-mailcow/', $_GET['service'])) {
+  if ($_GET['action'] == "start") {
+    header('Content-Type: text/html; charset=utf-8');
+    $retry = 0;
+    while (docker($_GET['service'], 'info')['State']['Running'] != 1 && $retry <= 3) {
+      $response = docker($_GET['service'], 'post', 'start');
+      $response = json_decode($response, true);
+      $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+      if ($response['type'] == "success") {
+        break;
+      }
+      usleep(1500000);
+      $retry++;
+    }
+    echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
+  }
+  if ($_GET['action'] == "stop") {
+    header('Content-Type: text/html; charset=utf-8');
+    $retry = 0;
+    while (docker($_GET['service'], 'info')['State']['Running'] == 1 && $retry <= 3) {
+      $response = docker($_GET['service'], 'post', 'stop');
+      $response = json_decode($response, true);
+      $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+      if ($response['type'] == "success") {
+        break;
+      }
+      usleep(1500000);
+      $retry++;
+    }
+    echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
+  }
+  if ($_GET['action'] == "restart") {
+    header('Content-Type: text/html; charset=utf-8');
+    $response = docker($_GET['service'], 'post', 'restart');
+    $response = json_decode($response, true);
+    $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+    echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Cannot restart container</span></b>' : $last_response;
+  }
+  if ($_GET['action'] == "logs") {
+    $lines = (empty($_GET['lines']) || !is_numeric($_GET['lines'])) ? 1000 : $_GET['lines'];
+    header('Content-Type: text/plain; charset=utf-8');
+    print_r(preg_split('/\n/', docker($_GET['service'], 'logs', $lines)));
+  }
+}
+
+?>

+ 402 - 0
data/web/inc/ajax/dns_diagnostics.php

@@ -0,0 +1,402 @@
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php';
+
+define('state_good', '<span class="glyphicon glyphicon-ok text-success"></span>');
+define('state_missing', '<span class="glyphicon glyphicon-remove text-danger"></span>');
+define('state_nomatch', "?");
+define('state_optional', " <sup>2</sup>");
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
+
+$domains = mailbox('get', 'domains');
+foreach(mailbox('get', 'domains') as $dn) {
+  $domains = array_merge($domains, mailbox('get', 'alias_domains', $dn));
+}
+
+if (isset($_GET['domain'])) {
+  if (is_valid_domain_name($_GET['domain'])) {
+    if (in_array($_GET['domain'], $domains)) {
+      $domain = $_GET['domain'];
+    }
+    else {
+      echo "No such domain in context";
+      die();
+    }
+  }
+  else {
+    echo "Invalid domain name";
+    die();
+  }
+}
+
+$ch = curl_init('http://ip4.mailcow.email');
+curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+curl_setopt($ch, CURLOPT_VERBOSE, false);
+curl_setopt($ch, CURLOPT_HEADER, false);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
+$ip = curl_exec($ch);
+curl_close($ch);
+
+$ch = curl_init('http://ip6.mailcow.email');
+curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
+curl_setopt($ch, CURLOPT_VERBOSE, false);
+curl_setopt($ch, CURLOPT_HEADER, false);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
+$ip6 = curl_exec($ch);
+curl_close($ch);
+
+$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
+if (!empty($ip6)) {
+  $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6);
+  $ip6_full = str_replace('::', ':0:', $ip6_full);
+  $ip6_full = str_replace('::', ':0:', $ip6_full);
+  $ptr6 = '';
+  foreach (explode(':', $ip6_full) as $part) {
+    $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT);
+  }
+  $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa';
+}
+
+$https_port = strpos($_SERVER['HTTP_HOST'], ':');
+if ($https_port === FALSE) {
+  $https_port = 443;
+}
+else {
+  $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1);
+}
+
+if (!isset($autodiscover_config['sieve'])) {
+  $autodiscover_config['sieve'] = array('server' => $mailcow_hostname, 'port' => array_pop(explode(':', getenv('SIEVE_PORT'))));
+}
+
+// Init records array
+$spf_link = '<a href="http://www.openspf.org/SPF_Record_Syntax" target="_blank">SPF Record Syntax</a>';
+$dmarc_link = '<a href="http://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
+
+$records = array();
+$records[] = array(
+  $mailcow_hostname,
+  'A',
+  $ip
+);
+$records[] = array(
+  $ptr,
+  'PTR',
+  $mailcow_hostname
+);
+if (!empty($ip6)) {
+  $records[] = array(
+    $mailcow_hostname,
+    'AAAA',
+    $ip6
+  );
+  $records[] = array(
+    $ptr6,
+    'PTR',
+    $mailcow_hostname
+  );
+}
+$records[] = array(
+	'_25._tcp.' . $autodiscover_config['smtp']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1)
+);
+$records[] = array(
+	'_' . $https_port . '._tcp.' . $mailcow_hostname,
+	'TLSA',
+	generate_tlsa_digest($mailcow_hostname, $https_port)
+);
+$records[] = array(
+	'_' . $autodiscover_config['pop3']['tlsport'] . '._tcp.' . $autodiscover_config['pop3']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['tlsport'], 1)
+);
+$records[] = array(
+	'_' . $autodiscover_config['imap']['tlsport'] . '._tcp.' . $autodiscover_config['imap']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['tlsport'], 1)
+);
+$records[] = array(
+	'_' . $autodiscover_config['smtp']['port'] . '._tcp.' . $autodiscover_config['smtp']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['port'])
+);
+$records[] = array(
+	'_' . $autodiscover_config['smtp']['tlsport'] . '._tcp.' . $autodiscover_config['smtp']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['tlsport'], 1)
+);
+$records[] = array(
+	'_' . $autodiscover_config['imap']['port'] . '._tcp.' . $autodiscover_config['imap']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['port'])
+);
+$records[] = array(
+	'_' . $autodiscover_config['pop3']['port'] . '._tcp.' . $autodiscover_config['pop3']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['port'])
+);
+$records[] = array(
+	'_' . $autodiscover_config['sieve']['port'] . '._tcp.' . $autodiscover_config['sieve']['server'],
+	'TLSA',
+	generate_tlsa_digest($autodiscover_config['sieve']['server'], $autodiscover_config['sieve']['port'], 1)
+);
+$records[] = array(
+	$domain,
+	'MX',
+	$mailcow_hostname
+);
+$records[] = array(
+	'autodiscover.' . $domain,
+	'CNAME',
+	$mailcow_hostname
+);
+$records[] = array(
+	'_autodiscover._tcp.' . $domain,
+	'SRV',
+	$mailcow_hostname . ' ' . $https_port
+);
+$records[] = array(
+	'autoconfig.' . $domain,
+	'CNAME',
+	$mailcow_hostname
+);
+$records[] = array(
+	$domain,
+	'TXT',
+	$spf_link,
+	state_optional
+);
+$records[] = array(
+	'_dmarc.' . $domain,
+	'TXT',
+	$dmarc_link,
+	state_optional
+);
+
+if (!empty($dkim = dkim('details', $domain))) {
+  $records[] = array(
+    $dkim['dkim_selector'] . '._domainkey.' . $domain,
+    'TXT',
+    $dkim['dkim_txt']
+  );
+}
+$current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV);
+if (count($current_records) == 0 || $current_records[0]['target'] != '') {
+  if ($autodiscover_config['pop3']['tlsport'] != '110') {
+    $records[] = array(
+      '_pop3._tcp.' . $domain,
+      'SRV',
+      $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']
+    );
+  }
+}
+else {
+  $records[] = array(
+    '_pop3._tcp.' . $domain,
+    'SRV',
+    '. 0'
+  );
+}
+$current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV);
+if (count($current_records) == 0 || $current_records[0]['target'] != '') {
+  if ($autodiscover_config['pop3']['port'] != '995') {
+    $records[] = array(
+      '_pop3s._tcp.' . $domain,
+      'SRV',
+      $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']
+    );
+  }
+}
+else {
+  $records[] = array(
+    '_pop3s._tcp.' . $domain,
+    'SRV',
+    '. 0'
+  );
+}
+if ($autodiscover_config['imap']['tlsport'] != '143') {
+  $records[] = array(
+    '_imap._tcp.' . $domain,
+    'SRV',
+    $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']
+  );
+}
+if ($autodiscover_config['imap']['port'] != '993') {
+  $records[] = array(
+    '_imaps._tcp.' . $domain,
+    'SRV',
+    $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']
+  );
+}
+if ($autodiscover_config['smtp']['tlsport'] != '587') {
+  $records[] = array(
+    '_submission._tcp.' . $domain,
+    'SRV',
+    $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']
+  );
+}
+if ($autodiscover_config['smtp']['port'] != '465') {
+  $records[] = array(
+    '_smtps._tcp.' . $domain,
+    'SRV',
+    $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']
+  );
+}
+if ($autodiscover_config['sieve']['port'] != '4190') {
+  $records[] = array(
+    '_sieve._tcp.' . $domain,
+    'SRV',
+    $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']
+  );
+}
+
+$record_types = array(
+  'A' => DNS_A,
+  'AAAA' => DNS_AAAA,
+  'CNAME' => DNS_CNAME,
+  'MX' => DNS_MX,
+  'PTR' => DNS_PTR,
+  'SRV' => DNS_SRV,
+  'TXT' => DNS_TXT,
+);
+$data_field = array(
+  'A' => 'ip',
+  'AAAA' => 'ipv6',
+  'CNAME' => 'target',
+  'MX' => 'target',
+  'PTR' => 'target',
+  'SRV' => 'data',
+  'TLSA' => 'data',
+  'TXT' => 'txt',
+);
+?>
+<div class="table-responsive" id="dnstable">
+  <table class="table table-striped">
+    <tr>
+      <th><?=$lang['diagnostics']['dns_records_name'];?></th>
+      <th><?=$lang['diagnostics']['dns_records_type'];?></th>
+      <th><?=$lang['diagnostics']['dns_records_data'];?></th>
+      <th><?=$lang['diagnostics']['dns_records_status'];?></th>
+    </tr>
+<?php
+foreach ($records as $record) {
+  $record[1] = strtoupper($record[1]);
+  $state = state_missing;
+  if ($record[1] == 'TLSA') {
+    $currents = dns_get_record($record[0], 52, $_, $_, TRUE);
+    foreach ($currents as &$current) {
+      $current['type'] = 'TLSA';
+      $current['cert_usage'] = hexdec(bin2hex($current['data']{0}));
+      $current['selector'] = hexdec(bin2hex($current['data']{1}));
+      $current['match_type'] = hexdec(bin2hex($current['data']{2}));
+      $current['cert_data'] = bin2hex(substr($current['data'], 3));
+      $current['data'] = $current['cert_usage'] . ' ' . $current['selector'] . ' ' . $current['match_type'] . ' ' . $current['cert_data'];
+    }
+    unset($current);
+  }
+  else {
+    $currents = dns_get_record($record[0], $record_types[$record[1]]);
+    if ($record[1] == 'SRV') {
+      foreach ($currents as &$current) {
+        if ($current['target'] == '') {
+          $current['target'] = '.';
+          $current['port'] = '0';
+        }
+        $current['data'] = $current['target'] . ' ' . $current['port'];
+      }
+      unset($current);
+    }
+    elseif ($record[1] == 'TXT') {
+      foreach ($currents as &$current) {
+        unset($current);
+      }
+      unset($current);
+    }
+  }
+
+  if ($record[1] == 'CNAME' && count($currents) == 0) {
+    // A and AAAA are also valid instead of CNAME
+    $a = dns_get_record($record[0], DNS_A);
+    $cname = dns_get_record($record[2], DNS_A);
+    if (count($a) > 0 && count($cname) > 0) {
+      if ($a[0]['ip'] == $cname[0]['ip']) {
+        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $record[2]));
+        $aaaa = dns_get_record($record[0], DNS_AAAA);
+        $cname = dns_get_record($record[2], DNS_AAAA);
+        if (count($aaaa) == 0 || count($cname) == 0 || $aaaa[0]['ipv6'] != $cname[0]['ipv6']) {
+          $currents[0]['target'] = $aaaa[0]['ipv6'] . ' <sup>1</sup>';
+        }
+      }
+      else {
+        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $a[0]['ip'] . ' <sup>1</sup>'));
+      }
+    }
+  }
+
+  foreach ($currents as &$current) {
+    if ($current['type'] == 'TXT' &&
+      stripos($current['txt'], 'v=dmarc') === 0 &&
+      $record[2] == $dmarc_link) {
+        $current['txt'] = str_replace(' ', '', $current['txt']);
+        $state = $current[$data_field[$current['type']]] . state_optional;
+    }
+    elseif ($current['type'] == 'TXT' &&
+      stripos($current['txt'], 'v=spf' &&
+      $record[2] == $spf_link) === 0) {
+        $state = $current[$data_field[$current['type']]] . state_optional;
+    }
+    elseif ($current['type'] == 'TXT' &&
+      stripos($current['txt'], 'v=dkim') === 0 &&
+      stripos($record[2], 'v=dkim') === 0) {
+        $current['txt'] = str_replace(' ', '', $current['txt']);
+        if ($current[$data_field[$current['type']]] == $record[2]) {
+          $state = state_good;
+        }
+    }
+    elseif ($current['type'] != 'TXT' &&
+      isset($data_field[$current['type']]) && $state != state_good) {
+        $state = state_nomatch;
+        if ($current[$data_field[$current['type']]] == $record[2]) {
+          $state = state_good;
+        }
+    }
+  }
+  unset($current);
+
+  if (isset($record[3]) &&
+    $record[3] == state_optional &&
+    ($state == state_missing || $state == state_nomatch)) {
+      $state = state_optional;
+  }
+  
+  if ($state == state_nomatch) {
+    $state = array();
+    foreach ($currents as $current) {
+      $state[] = $current[$data_field[$current['type']]];
+    }
+    $state = implode('<br />', $state);
+  }
+  echo sprintf('<tr>
+    <td>%s</td>
+    <td>%s</td>
+    <td class="dns-found">%s</td>
+    <td class="dns-recommended">%s</td>
+  </tr>', $record[0], $record[1], $record[2], $state);
+}
+?>
+  </table>
+</div>
+<p class="help-block">
+<sup>1</sup> <?=$lang['diagnostics']['cname_from_a'];?><br />
+<sup>2</sup> <?=$lang['diagnostics']['optional'];?>
+</p>
+<?php
+} else {
+  echo "Session invalid";
+  die();
+}
+?>

+ 12 - 0
data/web/inc/ajax/log_driver.php

@@ -0,0 +1,12 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+header('Content-Type: text/plain');
+if (!isset($_SESSION['mailcow_cc_role'])) {
+	exit();
+}
+if (isset($_GET['type']) && isset($_GET['msg'])) {
+  global $mailcow_hostname;
+  //empty
+}
+?>

+ 83 - 0
data/web/inc/ajax/qitem_details.php

@@ -0,0 +1,83 @@
+<?php
+session_start();
+header("Content-Type: application/json");
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+if (!isset($_SESSION['mailcow_cc_role'])) {
+	exit();
+}
+function rrmdir($src) {
+  $dir = opendir($src);
+  while(false !== ( $file = readdir($dir)) ) {
+    if (( $file != '.' ) && ( $file != '..' )) {
+      $full = $src . '/' . $file;
+      if ( is_dir($full) ) {
+        rrmdir($full);
+      }
+      else {
+        unlink($full);
+      }
+    }
+  }
+  closedir($dir);
+  rmdir($src);
+}
+if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) {
+  $tmpdir = '/tmp/' . $_GET['id'] . '/';
+  $mailc = quarantaine('details', $_GET['id']);
+  if (strlen($mailc['msg']) > 10485760) {
+    echo json_encode(array('error' => 'Message size exceeds 10 MiB.'));
+    exit;
+  }
+  if (!empty($mailc['msg'])) {
+    // Init message array
+    $data = array();
+    // Init parser
+    $mail_parser = new PhpMimeMailParser\Parser();
+    // Load msg to parser
+    $mail_parser->setText($mailc['msg']);
+    // Get text/plain content
+    $data['text_plain'] = $mail_parser->getMessageBody('text');
+    // Get subject
+    $data['subject'] = $mail_parser->getHeader('subject');
+    // Get attachments
+    if (is_dir($tmpdir)) {
+      rrmdir($tmpdir);
+    }
+    mkdir('/tmp/' . $_GET['id']);
+    $mail_parser->saveAttachments($tmpdir, true);
+    $atts = $mail_parser->getAttachments(true);
+    if (count($atts) > 0) {
+      foreach ($atts as $key => $val) {
+        $data['attachments'][$key] = array(
+          // Index
+          // 0 => file name
+          // 1 => mime type
+          // 2 => file size
+          // 3 => vt link by sha256
+          $val->getFilename(),
+          $val->getContentType(),
+          filesize($tmpdir . $val->getFilename()),
+          'https://www.virustotal.com/file/' . hash_file('SHA256', $tmpdir . $val->getFilename()) . '/analysis/'
+        );
+      }
+    }
+    if (isset($_GET['att'])) {
+      $dl_id = intval($_GET['att']);
+      $dl_filename = $data['attachments'][$dl_id][0];
+      if (!is_dir($tmpdir . $dl_filename) && file_exists($tmpdir . $dl_filename)) {
+        header('Pragma: public');
+        header('Expires: 0');
+        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+        header('Cache-Control: private', false);
+        header('Content-Type: ' . $data['attachments'][$dl_id][1]);
+        header('Content-Disposition: attachment; filename="'. $dl_filename . '";');
+        header('Content-Transfer-Encoding: binary');
+        header('Content-Length: ' . $data['attachments'][$dl_id][2]);
+        readfile($tmpdir . $dl_filename);
+        exit;
+      }
+    }
+    echo json_encode($data);
+  }
+}
+?>

+ 0 - 39
data/web/inc/ajax/sogo_ctrl.php

@@ -1,39 +0,0 @@
-<?php
-session_start();
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
-header('Content-Type: text/html; charset=utf-8');
-if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
-	exit();
-}
-
-if ($_GET['ACTION'] == "start") {
-  $retry = 0;
-  while (docker('sogo-mailcow', 'info')['State']['Running'] != 1 && $retry <= 3) {
-    $response = docker('sogo-mailcow', 'post', 'start');
-    $response = json_decode($response, true);
-    $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
-    if ($response['type'] == "success") {
-      break;
-    }
-    usleep(1500000);
-    $retry++;
-  }
-  echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
-}
-
-if ($_GET['ACTION'] == "stop") {
-  $retry = 0;
-  while (docker('sogo-mailcow', 'info')['State']['Running'] == 1 && $retry <= 3) {
-    $response = docker('sogo-mailcow', 'post', 'stop');
-    $response = json_decode($response, true);
-    $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
-    if ($response['type'] == "success") {
-      break;
-    }
-    usleep(1500000);
-    $retry++;
-  }
-  echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
-}
-
-?>

+ 69 - 64
data/web/inc/footer.inc.php

@@ -8,10 +8,12 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/footer.php';
 <script src="/js/bootstrap-select.min.js"></script>
 <script src="/js/bootstrap-filestyle.min.js"></script>
 <script src="/js/notifications.min.js"></script>
+<script src="/js/formcache.min.js"></script>
 <script src="/js/numberedtextarea.min.js"></script>
 <script src="/js/u2f-api.js"></script>
 <script src="/js/api.js"></script>
 <script>
+var loading_text = '<?= $lang['footer']['loading']; ?>'
 $(window).scroll(function() {
   sessionStorage.scrollTop = $(this).scrollTop();
 });
@@ -26,11 +28,19 @@ $(document).ready(function() {
     msg = $('<span/>').html(message).text();
     if (type == 'danger') {
       auto_hide = 0;
+      $('#' + localStorage.getItem("add_modal")).modal('show');
+      localStorage.removeItem("add_modal");
     } else {
       auto_hide = 5000;
     }
+    $.ajax({
+      url: '/inc/ajax/log_driver.php',
+      data: {"type": type,"msg": msg},
+      type: "GET"
+    });
     $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
   }
+  $('[data-cached-form="true"]').formcache({key: $(this).data('id')});
   <?php if (isset($_SESSION['return'])): ?>
   mailcow_alert_box(<?=json_encode($_SESSION['return']['msg']); ?>,  "<?= $_SESSION['return']['type']; ?>");
   <?php endif; unset($_SESSION['return']); ?>
@@ -40,6 +50,7 @@ $(document).ready(function() {
     backdrop: 'static',
     keyboard: false
   });
+  $('#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();
       // If U2F
@@ -49,20 +60,21 @@ $(document).ready(function() {
           cache: false,
           dataType: 'script',
           url: "/api/v1/get/u2f-authentication/<?= (isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null; ?>",
-          success: function(data){
+          complete: function(data){
+            $('#u2f_status_auth').html('<?=$lang['tfa']['waiting_usb_auth'];?>');
             data;
+            setTimeout(function() {
+              console.log("Ready to authenticate");
+              u2f.sign(appId, challenge, registeredKeys, function(data) {
+                var form = document.getElementById('u2f_auth_form');
+                var auth = document.getElementById('u2f_auth_data');
+                console.log("Authenticate callback", data);
+                auth.value = JSON.stringify(data);
+                form.submit();
+              });
+            }, 1000);
           }
         });
-        setTimeout(function() {
-          console.log("sign: ", req);
-          u2f.sign(req, function(data) {
-            var form = document.getElementById('u2f_auth_form');
-            var auth = document.getElementById('u2f_auth_data');
-            console.log("Authenticate callback", data);
-            auth.value = JSON.stringify(data);
-            form.submit();
-          });
-        }, 1000);
       }
   });
   <?php endif; ?>
@@ -81,32 +93,34 @@ $(document).ready(function() {
     if ($(this).val() == "u2f") {
       $('#U2FModal').modal('show');
       $("option:selected").prop("selected", false);
+      $('#u2f_status_reg').html('<p><span class="glyphicon glyphicon-refresh glyphicon-spin"></span> Initializing, please wait...</p>');
       $.ajax({
         type: "GET",
         cache: false,
         dataType: 'script',
         url: "/api/v1/get/u2f-registration/<?= (isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null; ?>",
-        success: function(data){
+        complete: function(data){
           data;
+          setTimeout(function() {
+            console.log("Ready to 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');
+              console.log("Register callback: ", data);
+              if (deviceResponse.errorCode && deviceResponse.errorCode != 0) {
+                var u2f_return_code = document.getElementById('u2f_return_code');
+                u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
+                if (deviceResponse.errorCode == "4") { deviceResponse.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; }
+                u2f_return_code.innerHTML = 'Error code: ' + deviceResponse.errorCode;
+                return;
+              }
+              reg.value = JSON.stringify(deviceResponse);
+              form.submit();
+            });
+          }, 1000);
         }
       });
-      setTimeout(function() {
-        console.log("Register: ", req);
-        u2f.register([req], sigs, function(data) {
-          var form  = document.getElementById('u2f_reg_form');
-          var reg   = document.getElementById('u2f_register_data');
-          console.log("Register callback", data);
-          if (data.errorCode && data.errorCode != 0) {
-            var u2f_return_code = document.getElementById('u2f_return_code');
-            u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
-            if (data.errorCode == "4") { data.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; }
-            u2f_return_code.innerHTML = 'Error code: ' + data.errorCode;
-            return;
-          }
-          reg.value = JSON.stringify(data);
-          form.submit();
-        });
-      }, 1000);
     }
     if ($(this).val() == "none") {
       $('#DisableTFAModal').modal('show');
@@ -114,13 +128,8 @@ $(document).ready(function() {
     }
   });
 
-  // Activate tooltips
   $(function () {
     $('[data-toggle="tooltip"]').tooltip()
-  })
-  // Hide alerts after n seconds
-  $("#alert-fade").fadeTo(7000, 500).slideUp(500, function(){
-    $("#alert-fade").alert('close');
   });
 
   // Remember last navigation pill
@@ -149,7 +158,7 @@ $(document).ready(function() {
     }
   })();
 
-  // Disable submit after submitting form
+  // Disable submit after submitting form (not API driven buttons)
   $('form').submit(function() {
     if ($('form button[type="submit"]').data('submitted') == '1') {
       return false;
@@ -169,36 +178,32 @@ $(document).ready(function() {
   // Init Bootstrap Selectpicker
   $('select').selectpicker();
 
-  // Trigger SOGo restart
-  $('#triggerRestartSogo').click(function(){
-    $(this).prop("disabled",true);
-    $(this).html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> ');
-    $('#statusTriggerRestartSogo').text('Stopping SOGo workers, this may take a while... ');
-    $.ajax({
-      method: 'get',
-      url: '/inc/ajax/sogo_ctrl.php',
-      data: {
-        'ajax': true,
-        'ACTION': 'stop'
-      },
-      success: function(data) {
-        $('#statusTriggerRestartSogo').append(data);
-        $('#statusTriggerRestartSogo').append('<br>Starting SOGo...');
-        $.ajax({
-          method: 'get',
-          url: '/inc/ajax/sogo_ctrl.php',
-          data: {
-            'ajax': true,
-            'ACTION': 'start'
-          },
-          success: function(data) {
-            $('#statusTriggerRestartSogo').append(data);
-            $('#triggerRestartSogo').html('<span class="glyphicon glyphicon-ok"></span> ');
-          }
-        });
-      }
+  // 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').text('Restarting container, this may take a while... ');
+      $.ajax({
+        method: 'get',
+        url: '/inc/ajax/container_ctrl.php',
+        timeout: 10000,
+        data: {
+          'service': container,
+          'action': 'restart'
+        },
+        error: function() {
+          window.location = window.location.href.split("#")[0];
+        },
+        success: function(data) {
+          $('#statusTriggerRestartContainer').append(data);
+          $('#triggerRestartContainer').html('<span class="glyphicon glyphicon-ok"></span> ');
+        }
+      });
     });
-  });
+  })
 
   // CSRF
   $('<input type="hidden" value="<?= $_SESSION['CSRF']['TOKEN']; ?>">').attr('id', 'csrf_token').attr('name', 'csrf_token').appendTo('form');

+ 292 - 0
data/web/inc/functions.bcc.inc.php

@@ -0,0 +1,292 @@
+<?php
+function bcc($_action, $_data = null, $attr = null) {
+	global $pdo;
+	global $lang;
+  if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+    return false;
+  }
+  switch ($_action) {
+    case 'add':
+      if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      $local_dest = strtolower(trim($_data['local_dest']));
+      $bcc_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['bcc_dest']));
+      $active = intval($_data['active']);
+      $type = $_data['type'];
+      if ($type != 'sender' && $type != 'rcpt') {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Invalid BCC map type'
+        );
+        return false;
+      }
+      if (empty($bcc_dest)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'BCC destination cannot be empty'
+        );
+        return false;
+      }
+      if (is_valid_domain_name($local_dest)) {
+        if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['access_denied'])
+          );
+          return false;
+        }
+        $domain = idn_to_ascii($local_dest);
+        $local_dest_sane = '@' . idn_to_ascii($local_dest);
+      }
+      elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) {
+        if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['access_denied'])
+          );
+          return false;
+        }
+        $domain = mailbox('get', 'mailbox_details', $local_dest)['domain'];
+        if (empty($domain)) {
+          return false;
+        }
+        $local_dest_sane = $local_dest;
+      }
+      else {
+        return false;
+      }
+      foreach ($bcc_dest as &$bcc_dest_e) {
+        if (!filter_var($bcc_dest_e, FILTER_VALIDATE_EMAIL)) {
+          $bcc_dest_e = null;;
+        }
+        $bcc_dest_e = strtolower($bcc_dest_e);
+      }
+      $bcc_dest = array_filter($bcc_dest);
+      $bcc_dest = implode(",", $bcc_dest);
+      if (empty($bcc_dest)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'BCC map destination cannot be empty'
+        );
+        return false;
+      }
+      try {
+        $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
+          WHERE `local_dest` = :local_dest AND `type` = :type");
+        $stmt->execute(array(':local_dest' => $local_dest_sane, ':type' => $type));
+        $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      }
+      catch(PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+      if ($num_results != 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'A BCC map entry "' . htmlspecialchars($local_dest_sane) . '" exists for type "' . $type . '"'
+        );
+        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',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => 'BCC map entry saved'
+      );
+    break;
+    case 'edit':
+      if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      $ids = (array)$_data['id'];
+      foreach ($ids as $id) {
+        $is_now = bcc('details', $id);
+        if (!empty($is_now)) {
+          $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+          $bcc_dest = (!empty($_data['bcc_dest'])) ? $_data['bcc_dest'] : $is_now['bcc_dest'];
+          $local_dest = $is_now['local_dest'];
+          $type = (!empty($_data['type'])) ? $_data['type'] : $is_now['type'];
+        }
+        else {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['access_denied'])
+          );
+          return false;
+        }
+        $bcc_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $bcc_dest));
+        $active = intval($_data['active']);
+        foreach ($bcc_dest as &$bcc_dest_e) {
+          if (!filter_var($bcc_dest_e, FILTER_VALIDATE_EMAIL)) {
+            $bcc_dest_e = null;;
+          }
+          $bcc_dest_e = strtolower($bcc_dest_e);
+        }
+        $bcc_dest = array_filter($bcc_dest);
+        $bcc_dest = implode(",", $bcc_dest);
+        if (empty($bcc_dest)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'BCC map destination cannot be empty'
+          );
+          return false;
+        }
+        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'];
+        }
+        catch(PDOException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+          return false;
+        }
+        if (isset($id_now) && $id_now != $id) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'A BCC map entry ' . htmlspecialchars($local_dest) . ' exists for this type'
+          );
+          return false;
+        }
+        try {
+          $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) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+          return false;
+        }
+      }
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => 'BCC map entry edited'
+      );
+    break;
+    case 'details':
+      $bccdata = array();
+      $id = intval($_data);
+      try {
+        $stmt = $pdo->prepare("SELECT `id`,
+          `local_dest`,
+          `bcc_dest`,
+          `active` AS `active_int`,
+          CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+          `type`,
+          `created`,
+          `domain`,
+          `modified` FROM `bcc_maps`
+            WHERE `id` = :id");
+        $stmt->execute(array(':id' => $id));
+        $bccdata = $stmt->fetch(PDO::FETCH_ASSOC);
+      }
+      catch(PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+      if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $bccdata['domain'])) {
+        $bccdata = null;
+        return false;
+      }
+      return $bccdata;
+    break;
+    case 'get':
+      $bccdata = array();
+      $all_items = array();
+      $id = intval($_data);
+      try {
+        $stmt = $pdo->query("SELECT `id`, `domain` FROM `bcc_maps`");
+        $all_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      }
+      catch(PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+      foreach ($all_items as $i) {
+        if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $i['domain'])) {
+          $bccdata[] = $i['id'];
+        }
+      }
+      $all_items = null;
+      return $bccdata;
+    break;
+    case 'delete':
+      $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',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `id`= :id");
+          $stmt->execute(array(':id' => $id));
+        }
+        catch (PDOException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+          return false;
+        }
+      }
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => 'Deleted BCC map id/s ' . implode(', ', $ids)
+      );
+      return true;
+    break;
+  }
+}

+ 45 - 10
data/web/inc/functions.customize.inc.php

@@ -1,5 +1,4 @@
 <?php
-
 function customize($_action, $_item, $_data = null) {
 	global $redis;
 	global $lang;
@@ -19,7 +18,7 @@ function customize($_action, $_item, $_data = null) {
               if (file_exists($_data['main_logo']['tmp_name']) !== true) {
                 $_SESSION['return'] = array(
                   'type' => 'danger',
-                  'msg' => 'Cannot validate image file: Temporary file not found'
+                  'msg' => $lang['danger']['img_tmp_missing']
                 );
                 return false;
               }
@@ -27,7 +26,7 @@ function customize($_action, $_item, $_data = null) {
               if ($image->valid() !== true) {
                 $_SESSION['return'] = array(
                   'type' => 'danger',
-                  'msg' => 'Cannot validate image file'
+                  'msg' => $lang['danger']['img_invalid']
                 );
                 return false;
               }
@@ -36,7 +35,7 @@ function customize($_action, $_item, $_data = null) {
             catch (ImagickException $e) {
               $_SESSION['return'] = array(
                 'type' => 'danger',
-                'msg' => 'Cannot validate image file'
+                'msg' => $lang['danger']['img_invalid']
               );
               return false;
             }
@@ -44,7 +43,7 @@ function customize($_action, $_item, $_data = null) {
           else {
             $_SESSION['return'] = array(
               'type' => 'danger',
-              'msg' => 'Invalid mime type'
+              'msg' => $lang['danger']['invalid_mime_type']
             );
             return false;
           }
@@ -60,7 +59,7 @@ function customize($_action, $_item, $_data = null) {
           }
           $_SESSION['return'] = array(
             'type' => 'success',
-            'msg' => 'File uploaded successfully'
+            'msg' => $lang['success']['upload_success']
           );
         break;
       }
@@ -78,7 +77,7 @@ function customize($_action, $_item, $_data = null) {
           $apps = (array)$_data['app'];
           $links = (array)$_data['href'];
           $out = array();
-          if (count($apps) == count($links)) {;
+          if (count($apps) == count($links)) {
             for ($i = 0; $i < count($apps); $i++) {
               $out[] = array($apps[$i] => $links[$i]);
             }
@@ -95,7 +94,28 @@ function customize($_action, $_item, $_data = null) {
           }
           $_SESSION['return'] = array(
             'type' => 'success',
-            'msg' => 'Saved changes to app links'
+            'msg' => $lang['success']['app_links']
+          );
+        break;
+        case 'ui_texts':
+          $main_name = $_data['main_name'];
+          $apps_name = $_data['apps_name'];
+          $help_text = $_data['help_text'];
+          try {
+            $redis->set('MAIN_NAME', htmlspecialchars($main_name));
+            $redis->set('APPS_NAME', htmlspecialchars($apps_name));
+            $redis->set('HELP_TEXT', $help_text);
+          }
+          catch (RedisException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Redis: '.$e
+            );
+            return false;
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => $lang['success']['ui_texts']
           );
         break;
       }
@@ -114,7 +134,7 @@ function customize($_action, $_item, $_data = null) {
             if ($redis->del('MAIN_LOGO')) {
               $_SESSION['return'] = array(
                 'type' => 'success',
-                'msg' => 'Reset default logo'
+                'msg' => $lang['success']['reset_main_logo']
               );
               return true;
             }
@@ -156,6 +176,21 @@ function customize($_action, $_item, $_data = null) {
             return false;
           }
         break;
+        case 'ui_texts':
+          try {
+            $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
+            $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : 'mailcow Apps';
+            $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
+            return $data;
+          }
+          catch (RedisException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Redis: '.$e
+            );
+            return false;
+          }
+        break;
         case 'main_logo_specs':
           try {
             $image = new Imagick();
@@ -168,7 +203,7 @@ function customize($_action, $_item, $_data = null) {
           catch (ImagickException $e) {
             $_SESSION['return'] = array(
               'type' => 'danger',
-              'msg' => 'Error: Imagick exception while reading image'
+              'msg' => $lang['danger']['imagick_exception']
             );
             return false;
           }

+ 16 - 6
data/web/inc/functions.docker.inc.php

@@ -1,5 +1,12 @@
 <?php
-function docker($service_name, $action, $post_action = null, $post_fields = null) {
+function docker($service_name, $action, $attr1 = null, $attr2 = null, $extra_headers = null) {
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
   $curl = curl_init();
   curl_setopt($curl, CURLOPT_HTTPHEADER,array( 'Content-Type: application/json' ));
   switch($action) {
@@ -53,13 +60,16 @@ function docker($service_name, $action, $post_action = null, $post_fields = null
       }
     break;
     case 'post':
-      if (!empty($post_action)) {
+      if (!empty($attr1)) {
         $container_id = docker($service_name, 'get_id');
-        if (ctype_xdigit($container_id) && ctype_alnum($post_action)) {
-          curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $post_action);
+        if (ctype_xdigit($container_id) && ctype_alnum($attr1)) {
+          curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $attr1);
           curl_setopt($curl, CURLOPT_POST, 1);
-          if (!empty($post_fields)) {
-            curl_setopt( $curl, CURLOPT_POSTFIELDS, json_encode($post_fields));
+          if (!empty($attr2)) {
+            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($attr2));
+          }
+          if (!empty($extra_headers) && is_array($extra_headers)) {
+            curl_setopt($curl, CURLOPT_HTTPHEADER, $extra_headers);
           }
           curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
           $response = curl_exec($curl);

+ 1 - 1
data/web/inc/functions.fail2ban.inc.php

@@ -95,4 +95,4 @@ function fail2ban($_action, $_data = null) {
       );
     break;
   }
-}
+}

+ 203 - 10
data/web/inc/functions.inc.php

@@ -75,15 +75,15 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) {
   }
   if (empty($starttls)) {
     $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true)));
-    $stream = stream_socket_client('tls://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
-    // error_nr can be 0, so checking against error_msg
-    if ($error_msg) {
+    $stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
+    if (!$stream) {
+      $error_msg = isset($error_msg) ? $error_msg : '-';
       return $error_nr . ': ' . $error_msg;
     }
   }
   else {
     $stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
-    if ($error_msg) {
+    if (!$stream) {
       return $error_nr . ': ' . $error_msg;
     }
     $banner = fread($stream, 512 );
@@ -443,14 +443,31 @@ function user_get_alias_details($username) {
   }
   try {
     $data['address'] = $username;
-    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') AS `aliases` FROM `alias`
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') AS `shared_aliases` FROM `alias`
       WHERE `goto` REGEXP :username_goto
       AND `address` NOT LIKE '@%'
+      AND `goto` != :username_goto2
       AND `address` != :username_address");
-    $stmt->execute(array(':username_goto' => '(^|,)'.$username.'($|,)', ':username_address' => $username));
+    $stmt->execute(array(
+      ':username_goto' => '(^|,)'.$username.'($|,)',
+      ':username_goto2' => $username,
+      ':username_address' => $username
+      ));
     $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
     while ($row = array_shift($run)) {
-      $data['aliases'] = $row['aliases'];
+      $data['shared_aliases'] = $row['shared_aliases'];
+    }
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') AS `direct_aliases` FROM `alias`
+      WHERE `goto` = :username_goto
+      AND `address` != :username_address");
+    $stmt->execute(
+      array(
+      ':username_goto' => $username,
+      ':username_address' => $username
+      ));
+    $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($run)) {
+      $data['direct_aliases'] = $row['direct_aliases'];
     }
     $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ', '), '&#10008;') AS `ad_alias` FROM `mailbox`
       LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
@@ -466,7 +483,7 @@ function user_get_alias_details($username) {
     while ($row = array_shift($run)) {
       $data['aliases_also_send_as'] = $row['send_as'];
     }
-    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
+    $stmt = $pdo->prepare("SELECT IFNULL(CONCAT(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ', ', GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')), '&#10008;') AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` =  TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
     $stmt->execute(array(':username' => $username));
     $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
     while ($row = array_shift($run)) {
@@ -851,6 +868,135 @@ function verify_tfa_login($username, $token) {
 	}
   return false;
 }
+function admin_api($action, $data = null) {
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	switch ($action) {
+		case "edit":
+      $regen_key = $data['admin_api_regen_key'];
+      $active = (isset($data['active'])) ? 1 : 0;
+      $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
+      foreach ($allow_from as $key => $val) {
+        if (!filter_var($val, FILTER_VALIDATE_IP)) {
+          unset($allow_from[$key]);
+          continue;
+        }
+      }
+      $allow_from = implode(',', array_unique(array_filter($allow_from)));
+      if (empty($allow_from)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'List of allowed IPs cannot be empty'
+        );
+        return false;
+      }
+      $api_key = implode('-', array(
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3)))
+      ));
+      $stmt = $pdo->prepare("INSERT INTO `api` (`username`, `api_key`, `active`, `allow_from`)
+        SELECT `username`, :api_key, :active, :allow_from FROM `admin` WHERE `superadmin`='1' AND `active`='1'
+        ON DUPLICATE KEY UPDATE `active` = :active_u, `allow_from` = :allow_from_u ;");
+      $stmt->execute(array(
+        ':api_key' => $api_key,
+        ':active' => $active,
+        ':active_u' => $active,
+        ':allow_from' => $allow_from,
+        ':allow_from_u' => $allow_from
+      ));
+    break;
+    case "regen_key":
+      $api_key = implode('-', array(
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3))),
+        strtoupper(bin2hex(random_bytes(3)))
+      ));
+      $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `username` IN
+        (SELECT `username` FROM `admin` WHERE `superadmin`='1' AND `active`='1')");
+      $stmt->execute(array(
+        ':api_key' => $api_key
+      ));
+    break;
+  }
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['admin_modified'])
+	);
+}
+function rspamd_ui($action, $data = null) {
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	switch ($action) {
+		case "edit":
+      $rspamd_ui_pass = $data['rspamd_ui_pass'];
+      $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
+      if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Password cannot be empty'
+        );
+        return false;
+      }
+      if ($rspamd_ui_pass != $rspamd_ui_pass2) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Passwords do not match'
+        );
+        return false;
+      }
+      if (strlen($rspamd_ui_pass) < 6) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Please use at least 6 characters for your password'
+        );
+        return false;
+      }
+      $docker_return = docker('rspamd-mailcow', 'post', 'exec', array('cmd' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
+      if ($docker_return_array = json_decode($docker_return, true)) {
+        if ($docker_return_array['type'] == 'success') {
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => 'Rspamd UI password set successfully'
+          );
+          return true;
+        }
+        else {
+          $_SESSION['return'] = array(
+            'type' => $docker_return_array['type'],
+            'msg' => $docker_return_array['msg']
+          );
+          return false;
+        }
+      }
+      else {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Unknown error'
+        );
+        return false;
+      }
+    break;
+  }
+
+}
 function get_admin_details() {
   // No parameter to be given, only one admin should exist
 	global $pdo;
@@ -860,8 +1006,10 @@ function get_admin_details() {
     return false;
   }
   try {
-    $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin` WHERE `superadmin`='1' AND active='1'");
-    $stmt->execute();
+    $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`
+      WHERE `admin`.`superadmin`='1'
+        AND `admin`.`active`='1'");
     $data = $stmt->fetch(PDO::FETCH_ASSOC);
   }
   catch(PDOException $e) {
@@ -932,6 +1080,51 @@ function get_logs($container, $lines = false) {
       return $data_array;
     }
   }
+  if ($container == "watchdog-mailcow") {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('WATCHDOG_LOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('WATCHDOG_LOG', 0, intval($lines));
+    }
+    if ($data) {
+      foreach ($data as $json_line) {
+        $data_array[] = json_decode($json_line, true);
+      }
+      return $data_array;
+    }
+  }
+  if ($container == "acme-mailcow") {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('ACME_LOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('ACME_LOG', 0, intval($lines));
+    }
+    if ($data) {
+      foreach ($data as $json_line) {
+        $data_array[] = json_decode($json_line, true);
+      }
+      return $data_array;
+    }
+  }
+  if ($container == "api-mailcow") {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('API_LOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('API_LOG', 0, intval($lines));
+    }
+    if ($data) {
+      foreach ($data as $json_line) {
+        $data_array[] = json_decode($json_line, true);
+      }
+      return $data_array;
+    }
+  }
   if ($container == "fail2ban-mailcow") {
     if (!is_numeric($lines)) {
       list ($from, $to) = explode('-', $lines);

+ 44 - 20
data/web/inc/functions.mailbox.inc.php

@@ -333,6 +333,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
           $aliases			= $_data['aliases'];
           $mailboxes    = $_data['mailboxes'];
           $maxquota			= $_data['maxquota'];
+          $restart_sogo = $_data['restart_sogo'];
           $quota				= $_data['quota'];
           if ($maxquota > $quota) {
             $_SESSION['return'] = array(
@@ -416,10 +417,21 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               );
               return false;
             }
-            $_SESSION['return'] = array(
-              'type' => 'success',
-              'msg' => sprintf($lang['success']['domain_added'], htmlspecialchars($domain))
-            );
+            if (!empty($restart_sogo)) {
+              $restart_reponse = json_decode(docker('sogo-mailcow', 'post', 'restart'), true);
+              if ($restart_reponse['type'] == "success") {
+                $_SESSION['return'] = array(
+                  'type' => 'success',
+                  'msg' => sprintf($lang['success']['domain_added'], htmlspecialchars($domain))
+                );
+              }
+              else {
+                $_SESSION['return'] = array(
+                  'type' => 'warning',
+                  'msg' => 'Added domain but failed to restart SOGo, please check your server logs.'
+                );
+              }
+            }
           }
           catch (PDOException $e) {
             mailbox('delete', 'domain', array('domain' => $domain));
@@ -490,9 +502,20 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
             if (in_array($address, $gotos)) {
               continue;
             }
+            $domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
+            $local_part   = strstr($address, '@', true);
+            $address      = $local_part.'@'.$domain;
             $stmt = $pdo->prepare("SELECT `address` FROM `alias`
-              WHERE `address`= :address");
-            $stmt->execute(array(':address' => $address));
+              WHERE `address`= :address OR `address` IN (
+                SELECT `username` FROM `mailbox`, `alias_domain`
+                  WHERE (
+                    `alias_domain`.`alias_domain` = :address_d
+                      AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))");
+            $stmt->execute(array(
+              ':address' => $address,
+              ':address_l' => $local_part,
+              ':address_d' => $domain
+            ));
             $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
             if ($num_results != 0) {
               $_SESSION['return'] = array(
@@ -501,9 +524,6 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               );
               return false;
             }
-            $domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
-            $local_part   = strstr($address, '@', true);
-            $address      = $local_part.'@'.$domain;
             $domaindata = mailbox('get', 'domain_details', $domain);
             if (is_array($domaindata) && $domaindata['aliases_left'] == "0") {
               $_SESSION['return'] = array(
@@ -722,7 +742,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
           }
           $active = intval($_data['active']);
           $quota_b		= ($quota_m * 1048576);
-          $maildir		= $domain."/".$local_part."/";
+          $maildir		= $domain . "/" . $local_part . "/";
           if (!is_valid_domain_name($domain)) {
             $_SESSION['return'] = array(
               'type' => 'danger',
@@ -1405,7 +1425,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               $subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
               $enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
               $mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
-              $exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : '';
+              $exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude'];
               $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
             }
             else {
@@ -2302,7 +2322,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
           }
           else {
             try {
-              $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
+              $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND (`domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role)");
               $stmt->execute(array(
                 ':username' => $_SESSION['mailcow_cc_username'],
                 ':role' => $_SESSION['mailcow_cc_role'],
@@ -3103,10 +3123,6 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
           }
           foreach ($ids as $id) {
             if (!is_numeric($id)) {
-              $_SESSION['return'] = array(
-                'type' => 'danger',
-                'msg' => $id
-              );
               return false;
             }
             try {
@@ -3154,10 +3170,6 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
           }
           foreach ($ids as $id) {
             if (!is_numeric($id)) {
-              $_SESSION['return'] = array(
-                'type' => 'danger',
-                'msg' => $id
-              );
               return false;
             }
             try {
@@ -3366,6 +3378,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               $stmt->execute(array(
                 ':domain' => '%@'.$domain,
               ));
+              $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :domain");
+              $stmt->execute(array(
+                ':domain' => $domain,
+              ));
             }
             catch (PDOException $e) {
               $_SESSION['return'] = array(
@@ -3486,6 +3502,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               $stmt->execute(array(
                 ':alias_domain' => $alias_domain,
               ));
+              $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :alias_domain");
+              $stmt->execute(array(
+                ':alias_domain' => $alias_domain,
+              ));
             }
             catch (PDOException $e) {
               $_SESSION['return'] = array(
@@ -3580,6 +3600,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               $stmt->execute(array(
                 ':username' => $username
               ));
+              $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :username");
+              $stmt->execute(array(
+                ':username' => $username
+              ));
               $stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias`
                   WHERE `goto` REGEXP :username");
               $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));

+ 282 - 0
data/web/inc/functions.quarantaine.inc.php

@@ -0,0 +1,282 @@
+<?php
+function quarantaine($_action, $_data = null) {
+	global $pdo;
+	global $redis;
+	global $lang;
+  switch ($_action) {
+    case 'delete':
+      if (!is_array($_data['id'])) {
+        $ids = array();
+        $ids[] = $_data['id'];
+      }
+      else {
+        $ids = $_data['id'];
+      }
+      if (!isset($_SESSION['acl']['quarantaine']) || $_SESSION['acl']['quarantaine'] != "1" ) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      foreach ($ids as $id) {
+        if (!is_numeric($id)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['access_denied'])
+          );
+          return false;
+        }
+        try {
+          $stmt = $pdo->prepare('SELECT `rcpt` FROM `quarantaine` WHERE `id` = :id');
+          $stmt->execute(array(':id' => $id));
+          $row = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) {
+            try {
+              $stmt = $pdo->prepare("DELETE FROM `quarantaine` WHERE `id` = :id");
+              $stmt->execute(array(
+                ':id' => $id
+              ));
+            }
+            catch (PDOException $e) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'MySQL: '.$e
+              );
+              return false;
+            }
+          }
+          else {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+        }
+        catch(PDOException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+        }
+      }
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $ids))
+      );
+    break;
+    case 'edit':
+      if (!isset($_SESSION['acl']['quarantaine']) || $_SESSION['acl']['quarantaine'] != "1" ) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      // Edit settings
+      if ($_data['action'] == 'settings') {
+        if ($_SESSION['mailcow_cc_role'] != "admin") {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['access_denied'])
+          );
+          return false;
+        }
+        $retention_size = $_data['retention_size'];
+        $max_size = $_data['max_size'];
+        $exclude_domains = (array)$_data['exclude_domains'];
+        try {
+          $redis->Set('Q_RETENTION_SIZE', intval($retention_size));
+          $redis->Set('Q_MAX_SIZE', intval($max_size));
+          $redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains));
+        }
+        catch (RedisException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'Redis: '.$e
+          );
+          return false;
+        }
+        $_SESSION['return'] = array(
+          'type' => 'success',
+          'msg' => 'Saved settings'
+        );
+      }
+      // Release item
+      elseif ($_data['action'] == 'release') {
+        if (!is_array($_data['id'])) {
+          $ids = array();
+          $ids[] = $_data['id'];
+        }
+        else {
+          $ids = $_data['id'];
+        }
+        foreach ($ids as $id) {
+          if (!is_numeric($id)) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          try {
+            $stmt = $pdo->prepare('SELECT `msg`, `qid`, `sender`, `rcpt` FROM `quarantaine` WHERE `id` = :id');
+            $stmt->execute(array(':id' => $id));
+            $row = $stmt->fetch(PDO::FETCH_ASSOC);
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+          }
+          catch(PDOException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'MySQL: '.$e
+            );
+          }
+          $sender = (isset($row['sender'])) ? $row['sender'] : 'sender-unknown@rspamd';
+          try {
+            $mail = new PHPMailer(true);
+            $mail->isSMTP();
+            $mail->SMTPDebug = 0;
+            $mail->SMTPOptions = array(
+              'ssl' => array(
+                  'verify_peer' => false,
+                  'verify_peer_name' => false,
+                  'allow_self_signed' => true
+              )
+            );
+            if (!empty(gethostbynamel('postfix-mailcow'))) {
+              $postfix = 'postfix-mailcow';
+            }
+            if (!empty(gethostbynamel('postfix'))) {
+              $postfix = 'postfix';
+            }
+            else {
+              $_SESSION['return'] = array(
+                'type' => 'warning',
+                'msg' => sprintf($lang['danger']['release_send_failed'], 'Cannot determine Postfix host')
+              );
+              return false;
+            }
+            $mail->Host = $postfix;
+            $mail->Port = 590;
+            $mail->setFrom($sender);
+            $mail->CharSet = 'UTF-8';
+            $mail->Subject = sprintf($lang['quarantaine']['release_subject'], $row['qid']);
+            $mail->addAddress($row['rcpt']);
+            $mail->IsHTML(false);
+            $msg_tmpf = tempnam("/tmp", $row['qid']);
+            file_put_contents($msg_tmpf, $row['msg']);
+            $mail->addAttachment($msg_tmpf, $row['qid'] . '.eml');
+            $mail->Body = sprintf($lang['quarantaine']['release_body']);
+            $mail->send();
+            unlink($msg_tmpf);
+          }
+          catch (phpmailerException $e) {
+            unlink($msg_tmpf);
+            $_SESSION['return'] = array(
+              'type' => 'warning',
+              'msg' => sprintf($lang['danger']['release_send_failed'], $e->errorMessage())
+            );
+            return false;
+          }
+          try {
+            $stmt = $pdo->prepare("DELETE FROM `quarantaine` WHERE `id` = :id");
+            $stmt->execute(array(
+              ':id' => $id
+            ));
+          }
+          catch (PDOException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'MySQL: '.$e
+            );
+            return false;
+          }
+        }
+        $_SESSION['return'] = array(
+          'type' => 'success',
+          'msg' => $lang['success']['items_released']
+        );
+      }
+      return true;
+    break;
+    case 'get':
+      try {
+        if ($_SESSION['mailcow_cc_role'] == "user") {
+          $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine` WHERE `rcpt` = :mbox');
+          $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username']));
+          $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+          while($row = array_shift($rows)) {
+            $q_meta[] = $row;
+          }
+        }
+        else {
+          foreach (mailbox('get', 'mailboxes') as $mbox) {
+            $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine` WHERE `rcpt` = :mbox');
+            $stmt->execute(array(':mbox' => $mbox));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            while($row = array_shift($rows)) {
+              $q_meta[] = $row;
+            }
+          }
+        }
+      }
+      catch(PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+      }
+      return $q_meta;
+    break;
+    case 'settings':
+      if ($_SESSION['mailcow_cc_role'] != "admin") {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      try {
+        $settings['exclude_domains'] = json_decode($redis->Get('Q_EXCLUDE_DOMAINS'), true);
+        $settings['max_size'] = $redis->Get('Q_MAX_SIZE');
+        $settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE');
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Redis: '.$e
+        );
+        return false;
+      }
+      return $settings;
+    break;
+    case 'details':
+      if (!is_numeric($_data) || empty($_data)) {
+        return false;
+      }
+      try {
+        $stmt = $pdo->prepare('SELECT `rcpt`, `symbols`, `msg`, `domain` FROM `quarantaine` WHERE `id`= :id');
+        $stmt->execute(array(':id' => $_data));
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) {
+          return $row;
+        }
+        return false;
+      }
+      catch(PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+      }
+      return false;
+    break;
+  }
+}

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

@@ -56,8 +56,8 @@ function relayhost($_action, $_data = null) {
         $is_now = relayhost('details', $id);
         if (!empty($is_now)) {
           $hostname = (!empty($_data['hostname'])) ? trim($_data['hostname']) : $is_now['hostname'];
-          $username = (!empty($_data['username'])) ? trim($_data['username']) : $is_now['username'];
-          $password = (!empty($_data['password'])) ? trim($_data['password']) : $is_now['password'];
+          $username = (isset($_data['username'])) ? trim($_data['username']) : $is_now['username'];
+          $password = (isset($_data['password'])) ? trim($_data['password']) : $is_now['password'];
           $active   = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
         }
         else {

+ 11 - 3
data/web/inc/header.inc.php

@@ -15,6 +15,7 @@
 <?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">
@@ -27,6 +28,8 @@
 <?= (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("/quarantaine.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/quarantaine.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">
 </head>
@@ -35,7 +38,6 @@
   <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="sr-only">Toggle navigation</span>
         <span class="icon-bar"></span>
         <span class="icon-bar"></span>
         <span class="icon-bar"></span>
@@ -70,6 +72,7 @@
             if (isset($_SESSION['mailcow_cc_role'])) {
               if ($_SESSION['mailcow_cc_role'] == 'admin') {
               ?>
+                <li<?= (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/debug.php"><?= $lang['header']['debug']; ?></a></li>
                 <li<?= (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/admin.php"><?= $lang['header']['administration']; ?></a></li>
               <?php
               }
@@ -88,14 +91,19 @@
           </ul>
         </li>
         <?php
+        if (isset($_SESSION['mailcow_cc_role'])) {
+        ?>
+        <li<?= (preg_match("/quarantaine/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/quarantaine.php"><span style="font-size: 12px;" class="glyphicon glyphicon-briefcase"></span> <?= $lang['header']['quarantaine']; ?></a></li>
+        <?php
+        }
         if ($_SESSION['mailcow_cc_role'] == 'admin') {
         ?>
-        <li><a href data-toggle="modal" data-target="#RestartSOGo"><span style="font-size: 12px;" class="glyphicon glyphicon-refresh" aria-hidden="true"></span> <?= $lang['header']['restart_sogo']; ?></a></li>
+        <li><a href data-toggle="modal" data-container="sogo-mailcow" data-target="#RestartContainer"><span style="font-size: 12px;" class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_sogo']; ?></a></li>
         <?php
         }
         ?>
         <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="glyphicon glyphicon-link" aria-hidden="true"></span> Apps <span class="caret"></span></a>
+          <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):

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

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "14112017_2103";
+    $db_version = "02012018_1515";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -103,6 +103,30 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "api" => array(
+        "cols" => array(
+          "username" => "VARCHAR(255) NOT NULL",
+          "api_key" => "VARCHAR(255) NOT NULL",
+          "allow_from" => "VARCHAR(512) NOT NULL",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE NOW(0)",
+          "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("username")
+          ),
+          "fkey" => array(
+            "fk_username_api" => array(
+              "col" => "username",
+              "ref" => "admin.username",
+              "delete" => "CASCADE",
+              "update" => "NO ACTION"
+            )
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "sender_acl" => array(
         "cols" => array(
           "logged_in_as" => "VARCHAR(255) NOT NULL",
@@ -133,6 +157,28 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "quarantaine" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "qid" => "VARCHAR(30) NOT NULL",
+          "score" => "FLOAT(8,2)",
+          "ip" => "VARBINARY(16)",
+          "action" => "CHAR(20) NOT NULL DEFAULT 'unknown'",
+          "symbols" => "JSON",
+          "sender" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
+          "rcpt" => "VARCHAR(255)",
+          "msg" => "LONGTEXT",
+          "domain" => "VARCHAR(255)",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("id")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "mailbox" => array(
         "cols" => array(
           "username" => "VARCHAR(255) NOT NULL",
@@ -202,6 +248,8 @@ function init_db_schema() {
           "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "filters" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "quarantaine" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'",
         ),
         "keys" => array(
           "fkey" => array(
@@ -325,6 +373,27 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "bcc_maps" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "local_dest" => "VARCHAR(255) NOT NULL",
+          "bcc_dest" => "VARCHAR(255) NOT NULL",
+          "domain" => "VARCHAR(255) NOT NULL",
+          "type" => "ENUM('sender','rcpt')",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
+          "active" => "TINYINT(1) NOT NULL DEFAULT '0'"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("id")
+          ),
+          "key" => array(
+            "local_dest" => array("local_dest"),
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "tfa" => array(
         "cols" => array(
           "id" => "INT NOT NULL AUTO_INCREMENT",
@@ -471,12 +540,13 @@ function init_db_schema() {
           "c_sn" => "VARCHAR(255)",
           "c_screenname" => "VARCHAR(255)",
           "c_l" => "VARCHAR(255)",
-          "c_mail" => "VARCHAR(255)",
+          "c_mail" => "TEXT",
           "c_o" => "VARCHAR(255)",
           "c_ou" => "VARCHAR(255)",
           "c_telephonenumber" => "VARCHAR(255)",
           "c_categories" => "VARCHAR(255)",
-          "c_component" => "VARCHAR(10) NOT NULL"
+          "c_component" => "VARCHAR(10) NOT NULL",
+          "c_hascertificate" => "INT4 DEFAULT 0"
         ),
         "keys" => array(
           "primary" => array(
@@ -519,8 +589,8 @@ function init_db_schema() {
       "sogo_user_profile" => array(
         "cols" => array(
           "c_uid" => "VARCHAR(255) NOT NULL",
-          "c_defaults" => "TEXT",
-          "c_settings" => "TEXT"
+          "c_defaults" => "LONGTEXT",
+          "c_settings" => "LONGTEXT"
         ),
         "keys" => array(
           "primary" => array(
@@ -686,6 +756,20 @@ function init_db_schema() {
       $pdo->query($create);
     }
 
+    // Create events to clean database
+    $events[] = 'DROP EVENT IF EXISTS clean_spamalias;
+DELIMITER //
+CREATE EVENT clean_spamalias 
+ON SCHEDULE EVERY 1 DAY DO 
+BEGIN
+  DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
+END;
+//
+DELIMITER ;';
+    foreach ($events as $event) {
+      $pdo->exec($event);
+    }
+
     // Inject admin if not exists
     $stmt = $pdo->query("SELECT NULL FROM `admin`"); 
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));

+ 2 - 1
data/web/inc/lib/composer.json

@@ -2,6 +2,7 @@
     "require": {
         "robthree/twofactorauth": "^1.6",
         "yubico/u2flib-server": "^1.0",
-        "phpmailer/phpmailer": "^5.2"
+        "phpmailer/phpmailer": "^5.2",
+        "php-mime-mail-parser/php-mime-mail-parser": "^2.9"
     }
 }

+ 81 - 1
data/web/inc/lib/composer.lock

@@ -4,8 +4,88 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "d51ef1712a74b0dfed729f2bdd85d1e3",
+    "content-hash": "ee4c9e269c29282221ce88bc23f1bda9",
     "packages": [
+        {
+            "name": "php-mime-mail-parser/php-mime-mail-parser",
+            "version": "2.9.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-mime-mail-parser/php-mime-mail-parser.git",
+                "reference": "c6884c7bc77adbf55979db99841195b232fd30f1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/c6884c7bc77adbf55979db99841195b232fd30f1",
+                "reference": "c6884c7bc77adbf55979db99841195b232fd30f1",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mailparse": "*",
+                "php": "^5.4.0 || ^7.0"
+            },
+            "replace": {
+                "exorus/php-mime-mail-parser": "*",
+                "messaged/php-mime-mail-parser": "*"
+            },
+            "require-dev": {
+                "phpunit/php-token-stream": "^1.3.0",
+                "phpunit/phpunit": "^4.0 || ^5.0",
+                "satooshi/php-coveralls": "0.*",
+                "squizlabs/php_codesniffer": "2.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PhpMimeMailParser\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "bucabay",
+                    "email": "gabe@fijiwebdesign.com",
+                    "homepage": "http://www.fijiwebdesign.com",
+                    "role": "Developer"
+                },
+                {
+                    "name": "eXorus",
+                    "email": "exorus.spam@gmail.com",
+                    "homepage": "https://github.com/eXorus/",
+                    "role": "Developer"
+                },
+                {
+                    "name": "M.Valinskis",
+                    "email": "M.Valins@gmail.com",
+                    "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                    "role": "Developer"
+                },
+                {
+                    "name": "eugene.emmett.wood",
+                    "email": "gene_w@cementhorizon.com",
+                    "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                    "role": "Developer"
+                },
+                {
+                    "name": "alknetso",
+                    "email": "alkne@gmail.com",
+                    "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Fully Tested Mailparse Extension Wrapper for PHP 5.4+",
+            "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser",
+            "keywords": [
+                "MimeMailParser",
+                "mail",
+                "mailparse",
+                "mime"
+            ],
+            "time": "2017-11-02T05:49:00+00:00"
+        },
         {
             "name": "phpmailer/phpmailer",
             "version": "v5.2.25",

+ 1 - 0
data/web/inc/lib/vendor/composer/autoload_psr4.php

@@ -7,4 +7,5 @@ $baseDir = dirname($vendorDir);
 
 return array(
     'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'),
+    'PhpMimeMailParser\\' => array($vendorDir . '/php-mime-mail-parser/php-mime-mail-parser/src'),
 );

+ 8 - 0
data/web/inc/lib/vendor/composer/autoload_static.php

@@ -11,6 +11,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
         array (
             'RobThree\\Auth\\' => 14,
         ),
+        'P' => 
+        array (
+            'PhpMimeMailParser\\' => 18,
+        ),
     );
 
     public static $prefixDirsPsr4 = array (
@@ -18,6 +22,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
         array (
             0 => __DIR__ . '/..' . '/robthree/twofactorauth/lib',
         ),
+        'PhpMimeMailParser\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/php-mime-mail-parser/php-mime-mail-parser/src',
+        ),
     );
 
     public static $classMap = array (

+ 82 - 0
data/web/inc/lib/vendor/composer/installed.json

@@ -167,5 +167,87 @@
             }
         ],
         "description": "PHPMailer is a full-featured email creation and transfer class for PHP"
+    },
+    {
+        "name": "php-mime-mail-parser/php-mime-mail-parser",
+        "version": "2.9.3",
+        "version_normalized": "2.9.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-mime-mail-parser/php-mime-mail-parser.git",
+            "reference": "c6884c7bc77adbf55979db99841195b232fd30f1"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/c6884c7bc77adbf55979db99841195b232fd30f1",
+            "reference": "c6884c7bc77adbf55979db99841195b232fd30f1",
+            "shasum": ""
+        },
+        "require": {
+            "ext-mailparse": "*",
+            "php": "^5.4.0 || ^7.0"
+        },
+        "replace": {
+            "exorus/php-mime-mail-parser": "*",
+            "messaged/php-mime-mail-parser": "*"
+        },
+        "require-dev": {
+            "phpunit/php-token-stream": "^1.3.0",
+            "phpunit/phpunit": "^4.0 || ^5.0",
+            "satooshi/php-coveralls": "0.*",
+            "squizlabs/php_codesniffer": "2.*"
+        },
+        "time": "2017-11-02T05:49:00+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "PhpMimeMailParser\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "bucabay",
+                "email": "gabe@fijiwebdesign.com",
+                "homepage": "http://www.fijiwebdesign.com",
+                "role": "Developer"
+            },
+            {
+                "name": "eXorus",
+                "email": "exorus.spam@gmail.com",
+                "homepage": "https://github.com/eXorus/",
+                "role": "Developer"
+            },
+            {
+                "name": "M.Valinskis",
+                "email": "M.Valins@gmail.com",
+                "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                "role": "Developer"
+            },
+            {
+                "name": "eugene.emmett.wood",
+                "email": "gene_w@cementhorizon.com",
+                "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                "role": "Developer"
+            },
+            {
+                "name": "alknetso",
+                "email": "alkne@gmail.com",
+                "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                "role": "Developer"
+            }
+        ],
+        "description": "Fully Tested Mailparse Extension Wrapper for PHP 5.4+",
+        "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser",
+        "keywords": [
+            "MimeMailParser",
+            "mail",
+            "mailparse",
+            "mime"
+        ]
     }
 ]

+ 21 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Vincent Dauce
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 167 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/README.md

@@ -0,0 +1,167 @@
+# php-mime-mail-parser
+
+A fully tested mailparse extension wrapper for PHP 5.4+
+
+[![Latest Version](https://img.shields.io/packagist/v/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases)
+[![Total Downloads](https://img.shields.io/packagist/dt/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://packagist.org/packages/php-mime-mail-parser/php-mime-mail-parser)
+[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
+
+## Why?
+
+This extension can be used to...
+ * Parse and read email from Postfix
+ * Create webmail 
+ * Store email information such a subject, HTML body, attachments, and etc. into a database
+
+## Is it reliable?
+
+Yes. All known issues have been reproduced, fixed and tested.
+
+We use Travis CI to help ensure code quality. You can see real-time statistics below:
+
+[![Build Status](https://img.shields.io/travis/php-mime-mail-parser/php-mime-mail-parser/master.svg?style=flat-square)](https://travis-ci.org/php-mime-mail-parser/php-mime-mail-parser)
+[![Coverage](https://img.shields.io/coveralls/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://coveralls.io/r/php-mime-mail-parser/php-mime-mail-parser)
+[![Quality Score](https://img.shields.io/scrutinizer/g/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-mime-mail-parser/php-mime-mail-parser)
+
+## How do I install it?
+
+The easiest way is via [Composer](https://getcomposer.org/).
+
+To install the latest version of PHP MIME Mail Parser, run the command below:
+
+	composer require php-mime-mail-parser/php-mime-mail-parser
+
+## Requirements
+
+The following versions of PHP are supported:
+
+* PHP 5.4
+* PHP 5.5
+* PHP 5.6
+* PHP 7
+* HHVM
+
+```
+sudo apt install php-cli php-pear php-dev php-mbstring
+```
+
+Make sure you have the mailparse extension (http://php.net/manual/en/book.mailparse.php) properly installed. The command line `php -m | grep mailparse` need to return "mailparse" else install it:
+* PHP version > 7.0: mailparse
+* PHP version < 7.0: mailparse 2.1.6
+
+Follow this steps to install mailparse:
+
+* Compile in the temp folder the extension mailparse or mailparse-2.1.6 (workaround because pecl install doesn't work yet)
+```
+cd
+pecl download mailparse
+tar -xvf mailparse-3.0.2.tgz 
+cd mailparse-3.0.2/
+phpize
+./configure
+sed -i 's/#if\s!HAVE_MBSTRING/#ifndef MBFL_MBFILTER_H/' ./mailparse.c
+make
+sudo mv modules/mailparse.so /usr/lib/php/20160303/
+```
+* Add the extension mailparse and activate it
+```
+echo "extension=mailparse.so" | sudo tee /etc/php/7.1/mods-available/mailparse.ini
+sudo phpenmod mailparse
+```
+
+On Windows, you need to download mailparse DLL from http://pecl.php.net/package/mailparse and add the line "extension=php_mailparse.dll" to php.ini accordingly.
+
+## How do I use it?
+
+```php
+<?php
+// Include the library first
+require_once __DIR__.'/vendor/autoload.php';
+
+$path = 'path/to/mail.txt';
+$Parser = new PhpMimeMailParser\Parser();
+
+// There are four methods available to indicate which mime mail to parse.
+// You only need to use one of the following four:
+
+// 1. Specify a file path to the mime mail.
+$Parser->setPath($path); 
+
+// 2. Specify a php file resource (stream) to the mime mail.
+$Parser->setStream(fopen($path, "r"));
+
+// 3. Specify the raw mime mail text.
+$Parser->setText(file_get_contents($path));
+
+// 4.  Specify a stream to work with mail server
+$Parser->setStream(fopen("php://stdin", "r"));
+
+// Once we've indicated where to find the mail, we can parse out the data
+$to = $Parser->getHeader('to');             // "test" <test@example.com>, "test2" <test2@example.com>
+$addressesTo = $Parser->getAddresses('to'); //Return an array : [[test, test@example.com, false],[test2, test2@example.com, false]]
+
+$from = $Parser->getHeader('from');             // "test" <test@example.com>
+$addressesFrom = $Parser->getAddresses('from'); //Return an array : test, test@example.com, false
+
+$subject = $Parser->getHeader('subject');
+
+$text = $Parser->getMessageBody('text');
+
+$html = $Parser->getMessageBody('html');
+$htmlEmbedded = $Parser->getMessageBody('htmlEmbedded'); //HTML Body included data
+
+$stringHeaders = $Parser->getHeadersRaw();	// Get all headers as a string, no charset conversion
+$arrayHeaders = $Parser->getHeaders();		// Get all headers as an array, with charset conversion
+
+// Pass in a writeable path to save attachments
+$attach_dir = '/path/to/save/attachments/'; 	// Be sure to include the trailing slash
+$include_inline = true;  			// Optional argument to include inline attachments (default: true)
+$Parser->saveAttachments($attach_dir [,$include_inline]);
+
+// Get an array of Attachment items from $Parser
+$attachments = $Parser->getAttachments([$include_inline]);
+
+//  Loop through all the Attachments
+if (count($attachments) > 0) {
+	foreach ($attachments as $attachment) {
+		echo 'Filename : '.$attachment->getFilename().'<br />'; // logo.jpg
+		echo 'Filesize : '.filesize($attach_dir.$attachment->getFilename()).'<br />'; // 1000
+		echo 'Filetype : '.$attachment->getContentType().'<br />'; // image/jpeg
+		echo 'MIME part string : '.$attachment->getMimePartStr().'<br />'; // (the whole MIME part of the attachment)
+	}
+}
+
+?>
+```
+
+Next you need to forward emails to this script above. For that I'm using [Postfix](http://www.postfix.org/) like a mail server, you need to configure /etc/postfix/master.cf
+
+Add this line at the end of the file (specify myhook to send all emails to the script test.php)
+```
+myhook unix - n n - - pipe
+  				flags=F user=www-data argv=php -c /etc/php5/apache2/php.ini -f /var/www/test.php ${sender} ${size} ${recipient}
+```
+
+Edit this line (register myhook)
+```
+smtp      inet  n       -       -       -       -       smtpd
+        			-o content_filter=myhook:dummy
+```
+
+The php script must use the fourth method to work with this configuration.
+
+
+## Can I contribute?
+
+Feel free to contribute!
+
+	git clone https://github.com/php-mime-mail-parser/php-mime-mail-parser
+	cd php-mime-mail-parser
+	composer install
+	./vendor/bin/phpunit
+
+If you report an issue, please provide the raw email that triggered it. This helps us reproduce the issue and fix it more quickly.
+
+### License
+
+The php-mime-mail-parser/php-mime-mail-parser is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)

+ 61 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/composer.json

@@ -0,0 +1,61 @@
+{
+    "name": "php-mime-mail-parser/php-mime-mail-parser",
+    "type": "library",
+    "description": "Fully Tested Mailparse Extension Wrapper for PHP 5.4+",
+    "keywords": ["mime", "mail", "mailparse", "MimeMailParser"],
+    "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser",
+    "license": "MIT",
+    "authors": [      
+        {
+            "name":"eXorus",
+            "email":"exorus.spam@gmail.com",
+            "homepage":"https://github.com/eXorus/",
+            "role":"Developer"
+        },
+        {
+            "name":"M.Valinskis",
+            "email":"M.Valins@gmail.com",
+            "homepage":"https://code.google.com/p/php-mime-mail-parser",
+            "role":"Developer"
+        },
+        {
+            "name":"eugene.emmett.wood",
+            "email":"gene_w@cementhorizon.com",
+            "homepage":"https://code.google.com/p/php-mime-mail-parser",
+            "role":"Developer"
+        },
+        {
+            "name":"alknetso",
+            "email":"alkne@gmail.com",
+            "homepage":"https://code.google.com/p/php-mime-mail-parser",
+            "role":"Developer"
+        },
+        {
+            "name":"bucabay",
+            "email":"gabe@fijiwebdesign.com",
+            "homepage":"http://www.fijiwebdesign.com",
+            "role":"Developer"
+        }
+    ],
+    "repository":{
+        "type":"git",
+        "url":"https://github.com/php-mime-mail-parser/php-mime-mail-parser.git"
+    },
+    "require": {
+        "php":           "^5.4.0 || ^7.0",
+        "ext-mailparse": "*"
+    },
+    "require-dev": {
+        "phpunit/phpunit":              "^4.0 || ^5.0",
+        "phpunit/php-token-stream":     "^1.3.0",
+        "satooshi/php-coveralls":       "0.*",
+        "squizlabs/PHP_CodeSniffer":    "2.*"
+    },
+    "replace": {
+        "exorus/php-mime-mail-parser":   "*",
+        "messaged/php-mime-mail-parser": "*"
+    },
+    "autoload": {
+        "psr-4": { "PhpMimeMailParser\\": "src/" }
+    }
+}

+ 303 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/mailparse-stubs.php

@@ -0,0 +1,303 @@
+<?php
+/**
+ * @link http://php.net/manual/en/mailparse.constants.php
+ */
+define('MAILPARSE_EXTRACT_OUTPUT', 0);
+
+/**
+ * @link http://php.net/manual/en/mailparse.constants.php
+ */
+define('MAILPARSE_EXTRACT_STREAM', 1);
+
+/**
+ * @link http://php.net/manual/en/mailparse.constants.php
+ */
+define('MAILPARSE_EXTRACT_RETURN', 2);
+
+/**
+ * Parses a file. This is the optimal way of parsing a mail file that you have on
+ * disk.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-parse-file.php
+ *
+ * @param string $filename Path to the file holding the message. The file is opened
+ *                         and streamed through the parser
+ *
+ * @return resource Returns a MIME resource representing the structure, or false on error
+ */
+function mailparse_msg_parse_file($filename)
+{
+}
+
+/**
+ * .
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-get-part.php
+ *
+ * @param resource $mimemail A valid MIME resource
+ * @param string   $mimesection
+ *
+ * @return resource
+ */
+function mailparse_msg_get_part($mimemail, $mimesection)
+{
+}
+
+/**
+ * .
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-get-structure.php
+ *
+ * @param resource $mimemail A valid MIME resource
+ *
+ * @return array
+ */
+function mailparse_msg_get_structure($mimemail)
+{
+}
+
+/**
+ * .
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-get-part-data.php
+ *
+ * @param resource $mimemail A valid MIME resource
+ *
+ * @return array
+ */
+function mailparse_msg_get_part_data($mimemail)
+{
+}
+
+/**
+ * .
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-extract-part.php
+ *
+ * @param resource $mimemail A valid MIME resource
+ * @param string   $msgbody
+ * @param callable $callbackfunc
+ *
+ * @return void
+ */
+function mailparse_msg_extract_part($mimemail, $msgbody, $callbackfunc)
+{
+}
+
+/**
+ * Extracts/decodes a message section from the supplied filename.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-extract-part-file.php
+ *
+ * @param resource $mimemail     A valid MIME resource, created with
+ *                               mailparse_msg_create
+ * @param mixed    $filename     Can be a file name or a valid stream resource
+ * @param callable $callbackfunc If set, this must be either a valid callback that
+ *                               will be passed the extracted section, or null to make this function return the
+ *                               extracted section
+ *
+ * @return string If $callbackfunc is not null returns true on success
+ */
+function mailparse_msg_extract_part_file($mimemail, $filename, $callbackfunc = false)
+{
+}
+
+/**
+ * .
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-extract-whole-part-file.php
+ *
+ * @param resource $mimemail A valid MIME resource
+ * @param string   $filename
+ * @param callable $callbackfunc
+ *
+ * @return string
+ */
+function mailparse_msg_extract_whole_part_file($mimemail, $filename, $callbackfunc)
+{
+}
+
+/**
+ * Create a MIME mail resource.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-create.php
+ * @return resource Returns a handle that can be used to parse a message
+ */
+function mailparse_msg_create()
+{
+}
+
+/**
+ * Frees a MIME resource.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-free.php
+ *
+ * @param resource $mimemail A valid MIME resource allocated by
+ *                           mailparse_msg_create or mailparse_msg_parse_file
+ *
+ * @return bool
+ */
+function mailparse_msg_free($mimemail)
+{
+}
+
+/**
+ * Incrementally parse data into the supplied mime mail resource.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-msg-parse.php
+ *
+ * @param resource $mimemail A valid MIME resource
+ * @param string   $data
+ *
+ * @return bool
+ */
+function mailparse_msg_parse($mimemail, $data)
+{
+}
+
+/**
+ * Parses a RFC 822 compliant recipient list, such as that found in the To: header.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-rfc822-parse-addresses.php
+ *
+ * @param string $addresses A string containing addresses, like in: Wez Furlong
+ *                          wez@example.com, doe@example.com
+ *
+ * @return array Returns an array of associative arrays with the following keys for each
+ *         recipient: display The recipient name, for display purpose. If this part is not
+ *         set for a recipient, this key will hold the same value as address. address The
+ *         email address is_group true if the recipient is a newsgroup, false otherwise
+ */
+function mailparse_rfc822_parse_addresses($addresses)
+{
+}
+
+/**
+ * Figures out the best way of encoding the content read from the given file
+ * pointer.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-determine-best-xfer-encoding.php
+ *
+ * @param resource $fp A valid file pointer, which must be seek-able
+ *
+ * @return string Returns one of the character encodings supported by the mbstring module
+ */
+function mailparse_determine_best_xfer_encoding($fp)
+{
+}
+
+/**
+ * Streams data from the source file pointer, apply $encoding and write to the
+ * destination file pointer.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-stream-encode.php
+ *
+ * @param resource $sourcefp A valid file handle. The file is streamed through the
+ *                           parser
+ * @param resource $destfp   The destination file handle in which the encoded data
+ *                           will be written
+ * @param string   $encoding One of the character encodings supported by the mbstring
+ *                           module
+ *
+ * @return bool
+ */
+function mailparse_stream_encode($sourcefp, $destfp, $encoding)
+{
+}
+
+/**
+ * Scans the data from the given file pointer and extract each embedded uuencoded
+ * file into a temporary file.
+ *
+ * @link http://php.net/manual/en/functions.mailparse-uudecode-all.php
+ *
+ * @param resource $fp A valid file pointer
+ *
+ * @return array Returns an array of associative arrays listing filename information.
+ *         filename Path to the temporary file name created origfilename The original
+ *         filename, for uuencoded parts only The first filename entry is the message body.
+ *         The next entries are the decoded uuencoded files
+ */
+function mailparse_uudecode_all($fp)
+{
+}
+
+/**
+ * @return
+ */
+function mailparse_test()
+{
+}
+
+class mimemessage
+{
+    /**
+     * @return
+     */
+    public function mimemessage()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function get_child()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function get_child_count()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function get_parent()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function extract_headers()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function extract_body()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function enum_uue()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function extract_uue()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function remove()
+    {
+    }
+
+    /**
+     * @return
+     */
+    public function add_child()
+    {
+    }
+}

+ 6 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/phpunit.xml.dist

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit colors="true" bootstrap="vendor/autoload.php">
+    <testsuite name="eXorus PhpMimeMailParser Test Suite">
+        <directory suffix="Test.php">tests</directory>
+    </testsuite>
+</phpunit>

+ 183 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Attachment.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace PhpMimeMailParser;
+
+/**
+ * Attachment of php-mime-mail-parser
+ *
+ * Fully Tested Mailparse Extension Wrapper for PHP 5.4+
+ *
+ */
+class Attachment
+{
+    /**
+     * @var string $filename Filename
+     */
+    protected $filename;
+
+    /**
+     * @var string $contentType Mime Type
+     */
+    protected $contentType;
+
+    /**
+     * @var string $content File Content
+     */
+    protected $content;
+
+    /**
+     * @var string $contentDisposition Content-Disposition (attachment or inline)
+     */
+    protected $contentDisposition;
+
+    /**
+     * @var string $contentId Content-ID
+     */
+    protected $contentId;
+
+    /**
+     * @var array $headers An Array of the attachment headers
+     */
+    protected $headers;
+
+    /**
+     * @var resource $stream
+     */
+    protected $stream;
+
+    /**
+     * @var string $mimePartStr
+     */
+    protected $mimePartStr;
+
+    /**
+     * Attachment constructor.
+     *
+     * @param string   $filename
+     * @param string   $contentType
+     * @param resource $stream
+     * @param string   $contentDisposition
+     * @param string   $contentId
+     * @param array    $headers
+     * @param string   $mimePartStr
+     */
+    public function __construct(
+        $filename,
+        $contentType,
+        $stream,
+        $contentDisposition = 'attachment',
+        $contentId = '',
+        $headers = [],
+        $mimePartStr = ''
+    ) {
+        $this->filename = $filename;
+        $this->contentType = $contentType;
+        $this->stream = $stream;
+        $this->content = null;
+        $this->contentDisposition = $contentDisposition;
+        $this->contentId = $contentId;
+        $this->headers = $headers;
+        $this->mimePartStr = $mimePartStr;
+    }
+
+    /**
+     * retrieve the attachment filename
+     *
+     * @return string
+     */
+    public function getFilename()
+    {
+        return $this->filename;
+    }
+
+    /**
+     * Retrieve the Attachment Content-Type
+     *
+     * @return string
+     */
+    public function getContentType()
+    {
+        return $this->contentType;
+    }
+
+    /**
+     * Retrieve the Attachment Content-Disposition
+     *
+     * @return string
+     */
+    public function getContentDisposition()
+    {
+        return $this->contentDisposition;
+    }
+
+    /**
+     * Retrieve the Attachment Content-ID
+     *
+     * @return string
+     */
+    public function getContentID()
+    {
+        return $this->contentId;
+    }
+
+    /**
+     * Retrieve the Attachment Headers
+     *
+     * @return array
+     */
+    public function getHeaders()
+    {
+        return $this->headers;
+    }
+
+    /**
+     * Get a handle to the stream
+     *
+     * @return stream
+     */
+    public function getStream()
+    {
+        return $this->stream;
+    }
+
+    /**
+     * Read the contents a few bytes at a time until completed
+     * Once read to completion, it always returns false
+     *
+     * @param int $bytes (default: 2082)
+     *
+     * @return string|bool
+     */
+    public function read($bytes = 2082)
+    {
+        return feof($this->stream) ? false : fread($this->stream, $bytes);
+    }
+
+    /**
+     * Retrieve the file content in one go
+     * Once you retrieve the content you cannot use MimeMailParser_attachment::read()
+     *
+     * @return string
+     */
+    public function getContent()
+    {
+        if ($this->content === null) {
+            fseek($this->stream, 0);
+            while (($buf = $this->read()) !== false) {
+                $this->content .= $buf;
+            }
+        }
+
+        return $this->content;
+    }
+
+    /**
+     * Get mime part string for this attachment
+     *
+     * @return string
+     */
+    public function getMimePartStr()
+    {
+        return $this->mimePartStr;
+    }
+}

+ 338 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Charset.php

@@ -0,0 +1,338 @@
+<?php namespace PhpMimeMailParser;
+
+use PhpMimeMailParser\Contracts\CharsetManager;
+
+class Charset implements CharsetManager
+{
+    /**
+     * Charset Aliases
+     */
+    private $charsetAlias = [
+        'ascii'                    => 'us-ascii',
+        'us-ascii'                 => 'us-ascii',
+        'ansi_x3.4-1968'           => 'us-ascii',
+        '646'                      => 'us-ascii',
+        'iso-8859-1'               => 'ISO-8859-1',
+        'iso-8859-2'               => 'ISO-8859-2',
+        'iso-8859-3'               => 'ISO-8859-3',
+        'iso-8859-4'               => 'ISO-8859-4',
+        'iso-8859-5'               => 'ISO-8859-5',
+        'iso-8859-6'               => 'ISO-8859-6',
+        'iso-8859-6-i'             => 'ISO-8859-6-I',
+        'iso-8859-6-e'             => 'ISO-8859-6-E',
+        'iso-8859-7'               => 'ISO-8859-7',
+        'iso-8859-8'               => 'ISO-8859-8',
+        'iso-8859-8-i'             => 'ISO-8859-8',
+        'iso-8859-8-e'             => 'ISO-8859-8-E',
+        'iso-8859-9'               => 'ISO-8859-9',
+        'iso-8859-10'              => 'ISO-8859-10',
+        'iso-8859-11'              => 'ISO-8859-11',
+        'iso-8859-13'              => 'ISO-8859-13',
+        'iso-8859-14'              => 'ISO-8859-14',
+        'iso-8859-15'              => 'ISO-8859-15',
+        'iso-8859-16'              => 'ISO-8859-16',
+        'iso-ir-111'               => 'ISO-IR-111',
+        'iso-2022-cn'              => 'ISO-2022-CN',
+        'iso-2022-cn-ext'          => 'ISO-2022-CN',
+        'iso-2022-kr'              => 'ISO-2022-KR',
+        'iso-2022-jp'              => 'ISO-2022-JP',
+        'utf-16be'                 => 'UTF-16BE',
+        'utf-16le'                 => 'UTF-16LE',
+        'utf-16'                   => 'UTF-16',
+        'windows-1250'             => 'windows-1250',
+        'windows-1251'             => 'windows-1251',
+        'windows-1252'             => 'windows-1252',
+        'windows-1253'             => 'windows-1253',
+        'windows-1254'             => 'windows-1254',
+        'windows-1255'             => 'windows-1255',
+        'windows-1256'             => 'windows-1256',
+        'windows-1257'             => 'windows-1257',
+        'windows-1258'             => 'windows-1258',
+        'ibm866'                   => 'IBM866',
+        'ibm850'                   => 'IBM850',
+        'ibm852'                   => 'IBM852',
+        'ibm855'                   => 'IBM855',
+        'ibm857'                   => 'IBM857',
+        'ibm862'                   => 'IBM862',
+        'ibm864'                   => 'IBM864',
+        'utf-8'                    => 'UTF-8',
+        'utf-7'                    => 'UTF-7',
+        'shift_jis'                => 'Shift_JIS',
+        'big5'                     => 'Big5',
+        'euc-jp'                   => 'EUC-JP',
+        'euc-kr'                   => 'EUC-KR',
+        'gb2312'                   => 'GB2312',
+        'gb18030'                  => 'gb18030',
+        'viscii'                   => 'VISCII',
+        'koi8-r'                   => 'KOI8-R',
+        'koi8_r'                   => 'KOI8-R',
+        'cskoi8r'                  => 'KOI8-R',
+        'koi'                      => 'KOI8-R',
+        'koi8'                     => 'KOI8-R',
+        'koi8-u'                   => 'KOI8-U',
+        'tis-620'                  => 'TIS-620',
+        't.61-8bit'                => 'T.61-8bit',
+        'hz-gb-2312'               => 'HZ-GB-2312',
+        'big5-hkscs'               => 'Big5-HKSCS',
+        'gbk'                      => 'gbk',
+        'cns11643'                 => 'x-euc-tw',
+        'x-imap4-modified-utf7'    => 'x-imap4-modified-utf7',
+        'x-euc-tw'                 => 'x-euc-tw',
+        'x-mac-ce'                 => 'x-mac-ce',
+        'x-mac-turkish'            => 'x-mac-turkish',
+        'x-mac-greek'              => 'x-mac-greek',
+        'x-mac-icelandic'          => 'x-mac-icelandic',
+        'x-mac-croatian'           => 'x-mac-croatian',
+        'x-mac-romanian'           => 'x-mac-romanian',
+        'x-mac-cyrillic'           => 'x-mac-cyrillic',
+        'x-mac-ukrainian'          => 'x-mac-cyrillic',
+        'x-mac-hebrew'             => 'x-mac-hebrew',
+        'x-mac-arabic'             => 'x-mac-arabic',
+        'x-mac-farsi'              => 'x-mac-farsi',
+        'x-mac-devanagari'         => 'x-mac-devanagari',
+        'x-mac-gujarati'           => 'x-mac-gujarati',
+        'x-mac-gurmukhi'           => 'x-mac-gurmukhi',
+        'armscii-8'                => 'armscii-8',
+        'x-viet-tcvn5712'          => 'x-viet-tcvn5712',
+        'x-viet-vps'               => 'x-viet-vps',
+        'iso-10646-ucs-2'          => 'UTF-16BE',
+        'x-iso-10646-ucs-2-be'     => 'UTF-16BE',
+        'x-iso-10646-ucs-2-le'     => 'UTF-16LE',
+        'x-user-defined'           => 'x-user-defined',
+        'x-johab'                  => 'x-johab',
+        'latin1'                   => 'ISO-8859-1',
+        'iso_8859-1'               => 'ISO-8859-1',
+        'iso8859-1'                => 'ISO-8859-1',
+        'iso8859-2'                => 'ISO-8859-2',
+        'iso8859-3'                => 'ISO-8859-3',
+        'iso8859-4'                => 'ISO-8859-4',
+        'iso8859-5'                => 'ISO-8859-5',
+        'iso8859-6'                => 'ISO-8859-6',
+        'iso8859-7'                => 'ISO-8859-7',
+        'iso8859-8'                => 'ISO-8859-8',
+        'iso8859-9'                => 'ISO-8859-9',
+        'iso8859-10'               => 'ISO-8859-10',
+        'iso8859-11'               => 'ISO-8859-11',
+        'iso8859-13'               => 'ISO-8859-13',
+        'iso8859-14'               => 'ISO-8859-14',
+        'iso8859-15'               => 'ISO-8859-15',
+        'iso_8859-1:1987'          => 'ISO-8859-1',
+        'iso-ir-100'               => 'ISO-8859-1',
+        'l1'                       => 'ISO-8859-1',
+        'ibm819'                   => 'ISO-8859-1',
+        'cp819'                    => 'ISO-8859-1',
+        'csisolatin1'              => 'ISO-8859-1',
+        'latin2'                   => 'ISO-8859-2',
+        'iso_8859-2'               => 'ISO-8859-2',
+        'iso_8859-2:1987'          => 'ISO-8859-2',
+        'iso-ir-101'               => 'ISO-8859-2',
+        'l2'                       => 'ISO-8859-2',
+        'csisolatin2'              => 'ISO-8859-2',
+        'latin3'                   => 'ISO-8859-3',
+        'iso_8859-3'               => 'ISO-8859-3',
+        'iso_8859-3:1988'          => 'ISO-8859-3',
+        'iso-ir-109'               => 'ISO-8859-3',
+        'l3'                       => 'ISO-8859-3',
+        'csisolatin3'              => 'ISO-8859-3',
+        'latin4'                   => 'ISO-8859-4',
+        'iso_8859-4'               => 'ISO-8859-4',
+        'iso_8859-4:1988'          => 'ISO-8859-4',
+        'iso-ir-110'               => 'ISO-8859-4',
+        'l4'                       => 'ISO-8859-4',
+        'csisolatin4'              => 'ISO-8859-4',
+        'cyrillic'                 => 'ISO-8859-5',
+        'iso_8859-5'               => 'ISO-8859-5',
+        'iso_8859-5:1988'          => 'ISO-8859-5',
+        'iso-ir-144'               => 'ISO-8859-5',
+        'csisolatincyrillic'       => 'ISO-8859-5',
+        'arabic'                   => 'ISO-8859-6',
+        'iso_8859-6'               => 'ISO-8859-6',
+        'iso_8859-6:1987'          => 'ISO-8859-6',
+        'iso-ir-127'               => 'ISO-8859-6',
+        'ecma-114'                 => 'ISO-8859-6',
+        'asmo-708'                 => 'ISO-8859-6',
+        'csisolatinarabic'         => 'ISO-8859-6',
+        'csiso88596i'              => 'ISO-8859-6-I',
+        'csiso88596e'              => 'ISO-8859-6-E',
+        'greek'                    => 'ISO-8859-7',
+        'greek8'                   => 'ISO-8859-7',
+        'sun_eu_greek'             => 'ISO-8859-7',
+        'iso_8859-7'               => 'ISO-8859-7',
+        'iso_8859-7:1987'          => 'ISO-8859-7',
+        'iso-ir-126'               => 'ISO-8859-7',
+        'elot_928'                 => 'ISO-8859-7',
+        'ecma-118'                 => 'ISO-8859-7',
+        'csisolatingreek'          => 'ISO-8859-7',
+        'hebrew'                   => 'ISO-8859-8',
+        'iso_8859-8'               => 'ISO-8859-8',
+        'visual'                   => 'ISO-8859-8',
+        'iso_8859-8:1988'          => 'ISO-8859-8',
+        'iso-ir-138'               => 'ISO-8859-8',
+        'csisolatinhebrew'         => 'ISO-8859-8',
+        'csiso88598i'              => 'ISO-8859-8',
+        'iso-8859-8i'              => 'ISO-8859-8',
+        'logical'                  => 'ISO-8859-8',
+        'csiso88598e'              => 'ISO-8859-8-E',
+        'latin5'                   => 'ISO-8859-9',
+        'iso_8859-9'               => 'ISO-8859-9',
+        'iso_8859-9:1989'          => 'ISO-8859-9',
+        'iso-ir-148'               => 'ISO-8859-9',
+        'l5'                       => 'ISO-8859-9',
+        'csisolatin5'              => 'ISO-8859-9',
+        'unicode-1-1-utf-8'        => 'UTF-8',
+        'utf8'                     => 'UTF-8',
+        'x-sjis'                   => 'Shift_JIS',
+        'shift-jis'                => 'Shift_JIS',
+        'ms_kanji'                 => 'Shift_JIS',
+        'csshiftjis'               => 'Shift_JIS',
+        'windows-31j'              => 'Shift_JIS',
+        'cp932'                    => 'Shift_JIS',
+        'sjis'                     => 'Shift_JIS',
+        'cseucpkdfmtjapanese'      => 'EUC-JP',
+        'x-euc-jp'                 => 'EUC-JP',
+        'csiso2022jp'              => 'ISO-2022-JP',
+        'iso-2022-jp-2'            => 'ISO-2022-JP',
+        'csiso2022jp2'             => 'ISO-2022-JP',
+        'csbig5'                   => 'Big5',
+        'cn-big5'                  => 'Big5',
+        'x-x-big5'                 => 'Big5',
+        'zh_tw-big5'               => 'Big5',
+        'cseuckr'                  => 'EUC-KR',
+        'ks_c_5601-1987'           => 'EUC-KR',
+        'iso-ir-149'               => 'EUC-KR',
+        'ks_c_5601-1989'           => 'EUC-KR',
+        'ksc_5601'                 => 'EUC-KR',
+        'ksc5601'                  => 'EUC-KR',
+        'korean'                   => 'EUC-KR',
+        'csksc56011987'            => 'EUC-KR',
+        '5601'                     => 'EUC-KR',
+        'windows-949'              => 'EUC-KR',
+        'gb_2312-80'               => 'GB2312',
+        'iso-ir-58'                => 'GB2312',
+        'chinese'                  => 'GB2312',
+        'csiso58gb231280'          => 'GB2312',
+        'csgb2312'                 => 'GB2312',
+        'zh_cn.euc'                => 'GB2312',
+        'gb_2312'                  => 'GB2312',
+        'x-cp1250'                 => 'windows-1250',
+        'x-cp1251'                 => 'windows-1251',
+        'x-cp1252'                 => 'windows-1252',
+        'x-cp1253'                 => 'windows-1253',
+        'x-cp1254'                 => 'windows-1254',
+        'x-cp1255'                 => 'windows-1255',
+        'x-cp1256'                 => 'windows-1256',
+        'x-cp1257'                 => 'windows-1257',
+        'x-cp1258'                 => 'windows-1258',
+        'windows-874'              => 'windows-874',
+        'ibm874'                   => 'windows-874',
+        'dos-874'                  => 'windows-874',
+        'macintosh'                => 'macintosh',
+        'x-mac-roman'              => 'macintosh',
+        'mac'                      => 'macintosh',
+        'csmacintosh'              => 'macintosh',
+        'cp866'                    => 'IBM866',
+        'cp-866'                   => 'IBM866',
+        '866'                      => 'IBM866',
+        'csibm866'                 => 'IBM866',
+        'cp850'                    => 'IBM850',
+        '850'                      => 'IBM850',
+        'csibm850'                 => 'IBM850',
+        'cp852'                    => 'IBM852',
+        '852'                      => 'IBM852',
+        'csibm852'                 => 'IBM852',
+        'cp855'                    => 'IBM855',
+        '855'                      => 'IBM855',
+        'csibm855'                 => 'IBM855',
+        'cp857'                    => 'IBM857',
+        '857'                      => 'IBM857',
+        'csibm857'                 => 'IBM857',
+        'cp862'                    => 'IBM862',
+        '862'                      => 'IBM862',
+        'csibm862'                 => 'IBM862',
+        'cp864'                    => 'IBM864',
+        '864'                      => 'IBM864',
+        'csibm864'                 => 'IBM864',
+        'ibm-864'                  => 'IBM864',
+        't.61'                     => 'T.61-8bit',
+        'iso-ir-103'               => 'T.61-8bit',
+        'csiso103t618bit'          => 'T.61-8bit',
+        'x-unicode-2-0-utf-7'      => 'UTF-7',
+        'unicode-2-0-utf-7'        => 'UTF-7',
+        'unicode-1-1-utf-7'        => 'UTF-7',
+        'csunicode11utf7'          => 'UTF-7',
+        'csunicode'                => 'UTF-16BE',
+        'csunicode11'              => 'UTF-16BE',
+        'iso-10646-ucs-basic'      => 'UTF-16BE',
+        'csunicodeascii'           => 'UTF-16BE',
+        'iso-10646-unicode-latin1' => 'UTF-16BE',
+        'csunicodelatin1'          => 'UTF-16BE',
+        'iso-10646'                => 'UTF-16BE',
+        'iso-10646-j-1'            => 'UTF-16BE',
+        'latin6'                   => 'ISO-8859-10',
+        'iso-ir-157'               => 'ISO-8859-10',
+        'l6'                       => 'ISO-8859-10',
+        'csisolatin6'              => 'ISO-8859-10',
+        'iso_8859-15'              => 'ISO-8859-15',
+        'csisolatin9'              => 'ISO-8859-15',
+        'l9'                       => 'ISO-8859-15',
+        'ecma-cyrillic'            => 'ISO-IR-111',
+        'csiso111ecmacyrillic'     => 'ISO-IR-111',
+        'csiso2022kr'              => 'ISO-2022-KR',
+        'csviscii'                 => 'VISCII',
+        'zh_tw-euc'                => 'x-euc-tw',
+        'iso88591'                 => 'ISO-8859-1',
+        'iso88592'                 => 'ISO-8859-2',
+        'iso88593'                 => 'ISO-8859-3',
+        'iso88594'                 => 'ISO-8859-4',
+        'iso88595'                 => 'ISO-8859-5',
+        'iso88596'                 => 'ISO-8859-6',
+        'iso88597'                 => 'ISO-8859-7',
+        'iso88598'                 => 'ISO-8859-8',
+        'iso88599'                 => 'ISO-8859-9',
+        'iso885910'                => 'ISO-8859-10',
+        'iso885911'                => 'ISO-8859-11',
+        'iso885912'                => 'ISO-8859-12',
+        'iso885913'                => 'ISO-8859-13',
+        'iso885914'                => 'ISO-8859-14',
+        'iso885915'                => 'ISO-8859-15',
+        'tis620'                   => 'TIS-620',
+        'cp1250'                   => 'windows-1250',
+        'cp1251'                   => 'windows-1251',
+        'cp1252'                   => 'windows-1252',
+        'cp1253'                   => 'windows-1253',
+        'cp1254'                   => 'windows-1254',
+        'cp1255'                   => 'windows-1255',
+        'cp1256'                   => 'windows-1256',
+        'cp1257'                   => 'windows-1257',
+        'cp1258'                   => 'windows-1258',
+        'x-gbk'                    => 'gbk',
+        'windows-936'              => 'gbk',
+        'ansi-1251'                => 'windows-1251',
+    ];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decodeCharset($encodedString, $charset)
+    {
+        if (strtolower($charset) == 'utf-8' || strtolower($charset) == 'us-ascii') {
+            return $encodedString;
+        } else {
+            return iconv($this->getCharsetAlias($charset), 'UTF-8//TRANSLIT//IGNORE', $encodedString);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCharsetAlias($charset)
+    {
+        $charset = strtolower($charset);
+
+        if (array_key_exists($charset, $this->charsetAlias)) {
+            return $this->charsetAlias[$charset];
+        } else {
+            return null;
+        }
+    }
+}

+ 24 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Contracts/CharsetManager.php

@@ -0,0 +1,24 @@
+<?php namespace PhpMimeMailParser\Contracts;
+
+interface CharsetManager
+{
+
+    /**
+     * Decode the string from Charset
+     *
+     * @param string $encodedString The string in its original encoded state
+     * @param string $charset       The Charset header of the part.
+     *
+     * @return string The decoded string
+     */
+    public function decodeCharset($encodedString, $charset);
+
+    /**
+     * Get charset alias
+     *
+     * @param string $charset .
+     *
+     * @return string The charset alias
+     */
+    public function getCharsetAlias($charset);
+}

+ 8 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Exception.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace PhpMimeMailParser;
+
+class Exception extends \RuntimeException
+{
+
+}

+ 893 - 0
data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Parser.php

@@ -0,0 +1,893 @@
+<?php
+
+namespace PhpMimeMailParser;
+
+use PhpMimeMailParser\Contracts\CharsetManager;
+
+/**
+ * Parser of php-mime-mail-parser
+ *
+ * Fully Tested Mailparse Extension Wrapper for PHP 5.4+
+ *
+ */
+class Parser
+{
+    /**
+     * Attachment filename argument option for ->saveAttachments().
+     */
+    const ATTACHMENT_DUPLICATE_THROW  = 'DuplicateThrow';
+    const ATTACHMENT_DUPLICATE_SUFFIX = 'DuplicateSuffix';
+    const ATTACHMENT_RANDOM_FILENAME  = 'RandomFilename';
+
+    /**
+     * PHP MimeParser Resource ID
+     *
+     * @var resource $resource
+     */
+    protected $resource;
+
+    /**
+     * A file pointer to email
+     *
+     * @var resource $stream
+     */
+    protected $stream;
+
+    /**
+     * A text of an email
+     *
+     * @var string $data
+     */
+    protected $data;
+
+    /**
+     * Parts of an email
+     *
+     * @var array $parts
+     */
+    protected $parts;
+
+    /**
+     * @var CharsetManager object
+     */
+    protected $charset;
+
+    /**
+     * Parser constructor.
+     *
+     * @param CharsetManager|null $charset
+     */
+    public function __construct(CharsetManager $charset = null)
+    {
+        if ($charset == null) {
+            $charset = new Charset();
+        }
+
+        $this->charset = $charset;
+    }
+
+    /**
+     * Free the held resources
+     *
+     * @return void
+     */
+    public function __destruct()
+    {
+        // clear the email file resource
+        if (is_resource($this->stream)) {
+            fclose($this->stream);
+        }
+        // clear the MailParse resource
+        if (is_resource($this->resource)) {
+            mailparse_msg_free($this->resource);
+        }
+    }
+
+    /**
+     * Set the file path we use to get the email text
+     *
+     * @param string $path File path to the MIME mail
+     *
+     * @return Parser MimeMailParser Instance
+     */
+    public function setPath($path)
+    {
+        // should parse message incrementally from file
+        $this->resource = mailparse_msg_parse_file($path);
+        $this->stream = fopen($path, 'r');
+        $this->parse();
+
+        return $this;
+    }
+
+    /**
+     * Set the Stream resource we use to get the email text
+     *
+     * @param resource $stream
+     *
+     * @return Parser MimeMailParser Instance
+     * @throws Exception
+     */
+    public function setStream($stream)
+    {
+        // streams have to be cached to file first
+        $meta = @stream_get_meta_data($stream);
+        if (!$meta || !$meta['mode'] || $meta['mode'][0] != 'r' || $meta['eof']) {
+            throw new Exception(
+                'setStream() expects parameter stream to be readable stream resource.'
+            );
+        }
+
+        /** @var resource $tmp_fp */
+        $tmp_fp = tmpfile();
+        if ($tmp_fp) {
+            while (!feof($stream)) {
+                fwrite($tmp_fp, fread($stream, 2028));
+            }
+            fseek($tmp_fp, 0);
+            $this->stream = &$tmp_fp;
+        } else {
+            throw new Exception(
+                'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
+            );
+        }
+        fclose($stream);
+
+        $this->resource = mailparse_msg_create();
+        // parses the message incrementally (low memory usage but slower)
+        while (!feof($this->stream)) {
+            mailparse_msg_parse($this->resource, fread($this->stream, 2082));
+        }
+        $this->parse();
+
+        return $this;
+    }
+
+    /**
+     * Set the email text
+     *
+     * @param string $data
+     *
+     * @return Parser MimeMailParser Instance
+     */
+    public function setText($data)
+    {
+        if (!$data) {
+            throw new Exception('You must not call MimeMailParser::setText with an empty string parameter');
+        }
+        $this->resource = mailparse_msg_create();
+        // does not parse incrementally, fast memory hog might explode
+        mailparse_msg_parse($this->resource, $data);
+        $this->data = $data;
+        $this->parse();
+
+        return $this;
+    }
+
+    /**
+     * Parse the Message into parts
+     *
+     * @return void
+     */
+    protected function parse()
+    {
+        $structure = mailparse_msg_get_structure($this->resource);
+        $this->parts = [];
+        foreach ($structure as $part_id) {
+            $part = mailparse_msg_get_part($this->resource, $part_id);
+            $this->parts[$part_id] = mailparse_msg_get_part_data($part);
+        }
+    }
+
+    /**
+     * Retrieve a specific Email Header, without charset conversion.
+     *
+     * @param string $name Header name (case-insensitive)
+     *
+     * @return string
+     * @throws Exception
+     */
+    public function getRawHeader($name)
+    {
+        $name = strtolower($name);
+        if (isset($this->parts[1])) {
+            $headers = $this->getPart('headers', $this->parts[1]);
+
+            return (isset($headers[$name])) ? $headers[$name] : false;
+        } else {
+            throw new Exception(
+                'setPath() or setText() or setStream() must be called before retrieving email headers.'
+            );
+        }
+    }
+
+    /**
+     * Retrieve a specific Email Header
+     *
+     * @param string $name Header name (case-insensitive)
+     *
+     * @return string
+     */
+    public function getHeader($name)
+    {
+        $rawHeader = $this->getRawHeader($name);
+        if ($rawHeader === false) {
+            return false;
+        }
+
+        return $this->decodeHeader($rawHeader);
+    }
+
+    /**
+     * Retrieve all mail headers
+     *
+     * @return array
+     * @throws Exception
+     */
+    public function getHeaders()
+    {
+        if (isset($this->parts[1])) {
+            $headers = $this->getPart('headers', $this->parts[1]);
+            foreach ($headers as $name => &$value) {
+                if (is_array($value)) {
+                    foreach ($value as &$v) {
+                        $v = $this->decodeSingleHeader($v);
+                    }
+                } else {
+                    $value = $this->decodeSingleHeader($value);
+                }
+            }
+
+            return $headers;
+        } else {
+            throw new Exception(
+                'setPath() or setText() or setStream() must be called before retrieving email headers.'
+            );
+        }
+    }
+
+    /**
+     * Retrieve the raw mail headers as a string
+     *
+     * @return string
+     * @throws Exception
+     */
+    public function getHeadersRaw()
+    {
+        if (isset($this->parts[1])) {
+            return $this->getPartHeader($this->parts[1]);
+        } else {
+            throw new Exception(
+                'setPath() or setText() or setStream() must be called before retrieving email headers.'
+            );
+        }
+    }
+
+    /**
+     * Retrieve the raw Header of a MIME part
+     *
+     * @return String
+     * @param $part Object
+     * @throws Exception
+     */
+    protected function getPartHeader(&$part)
+    {
+        $header = '';
+        if ($this->stream) {
+            $header = $this->getPartHeaderFromFile($part);
+        } elseif ($this->data) {
+            $header = $this->getPartHeaderFromText($part);
+        }
+        return $header;
+    }
+
+    /**
+     * Retrieve the Header from a MIME part from file
+     *
+     * @return String Mime Header Part
+     * @param $part Array
+     */
+    protected function getPartHeaderFromFile(&$part)
+    {
+        $start = $part['starting-pos'];
+        $end = $part['starting-pos-body'];
+        fseek($this->stream, $start, SEEK_SET);
+        $header = fread($this->stream, $end-$start);
+        return $header;
+    }
+
+    /**
+     * Retrieve the Header from a MIME part from text
+     *
+     * @return String Mime Header Part
+     * @param $part Array
+     */
+    protected function getPartHeaderFromText(&$part)
+    {
+        $start = $part['starting-pos'];
+        $end = $part['starting-pos-body'];
+        $header = substr($this->data, $start, $end-$start);
+        return $header;
+    }
+
+    /**
+     * Checks whether a given part ID is a child of another part
+     * eg. an RFC822 attachment may have one or more text parts
+     *
+     * @param string $partId
+     * @param string $parentPartId
+     * @return bool
+     */
+    protected function partIdIsChildOfPart($partId, $parentPartId)
+    {
+        return substr($partId, 0, strlen($parentPartId)) == $parentPartId;
+    }
+
+    /**
+     * Whether the given part ID is a child of any attachment part in the message.
+     *
+     * @param string $checkPartId
+     * @return bool
+     */
+    protected function partIdIsChildOfAnAttachment($checkPartId)
+    {
+        foreach ($this->parts as $partId => $part) {
+            if ($this->getPart('content-disposition', $part) == 'attachment') {
+                if ($this->partIdIsChildOfPart($checkPartId, $partId)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the email message body in the specified format
+     *
+     * @param string $type text, html or htmlEmbedded
+     *
+     * @return false|string Body or False if not found
+     * @throws Exception
+     */
+    public function getMessageBody($type = 'text')
+    {
+        $body = false;
+        $mime_types = [
+        'text'         => 'text/plain',
+        'html'         => 'text/html',
+        'htmlEmbedded' => 'text/html',
+        ];
+
+        if (in_array($type, array_keys($mime_types))) {
+            $part_type  = $type === 'htmlEmbedded' ? 'html' : $type;
+            $inline_parts = $this->getInlineParts($part_type);
+            $body = empty($inline_parts) ? '' : $inline_parts[0];
+        } else {
+            throw new Exception(
+                'Invalid type specified for getMessageBody(). Expected: text, html or htmlEmbeded.'
+            );
+        }
+
+        if ($type == 'htmlEmbedded') {
+            $attachments = $this->getAttachments();
+            foreach ($attachments as $attachment) {
+                if ($attachment->getContentID() != '') {
+                    $body = str_replace(
+                        '"cid:'.$attachment->getContentID().'"',
+                        '"'.$this->getEmbeddedData($attachment->getContentID()).'"',
+                        $body
+                    );
+                }
+            }
+        }
+
+        return $body;
+    }
+
+    /**
+     * Returns the embedded data structure
+     *
+     * @param string $contentId Content-Id
+     *
+     * @return string
+     */
+    protected function getEmbeddedData($contentId)
+    {
+        foreach ($this->parts as $part) {
+            if ($this->getPart('content-id', $part) == $contentId) {
+                $embeddedData = 'data:';
+                $embeddedData .= $this->getPart('content-type', $part);
+                $embeddedData .= ';'.$this->getPart('transfer-encoding', $part);
+                $embeddedData .= ','.$this->getPartBody($part);
+                return $embeddedData;
+            }
+        }
+        return '';
+    }
+
+    /**
+     * Return an array with the following keys display, address, is_group
+     *
+     * @param string $name Header name (case-insensitive)
+     *
+     * @return array
+     */
+    public function getAddresses($name)
+    {
+        $value = $this->getHeader($name);
+
+        return mailparse_rfc822_parse_addresses($value);
+    }
+
+    /**
+     * Returns the attachments contents in order of appearance
+     *
+     * @return Attachment[]
+     */
+    public function getInlineParts($type = 'text')
+    {
+        $inline_parts = [];
+        $dispositions = ['inline'];
+        $mime_types = [
+            'text'         => 'text/plain',
+            'html'         => 'text/html',
+        ];
+
+        if (!in_array($type, array_keys($mime_types))) {
+            throw new Exception('Invalid type specified for getInlineParts(). "type" can either be text or html.');
+        }
+
+        foreach ($this->parts as $partId => $part) {
+            if ($this->getPart('content-type', $part) == $mime_types[$type]
+                && $this->getPart('content-disposition', $part) != 'attachment'
+                && !$this->partIdIsChildOfAnAttachment($partId)
+                ) {
+                $headers = $this->getPart('headers', $part);
+                $encodingType = array_key_exists('content-transfer-encoding', $headers) ?
+                    $headers['content-transfer-encoding'] : '';
+                if (is_array($encodingType)) {
+                    $encodingType = $encodingType[0];
+                }
+                $undecoded_body = $this->decodeContentTransfer($this->getPartBody($part), $encodingType);
+                $inline_parts[] = $this->charset->decodeCharset($undecoded_body, $this->getPartCharset($part));
+            }
+        }
+
+        return $inline_parts;
+    }
+
+    /**
+     * Returns the attachments contents in order of appearance
+     *
+     * @return Attachment[]
+     */
+    public function getAttachments($include_inline = true)
+    {
+        $attachments = [];
+        $dispositions = $include_inline ?
+            ['attachment', 'inline'] :
+            ['attachment'];
+        $non_attachment_types = ['text/plain', 'text/html'];
+        $nonameIter = 0;
+
+        foreach ($this->parts as $part) {
+            $disposition = $this->getPart('content-disposition', $part);
+            $filename = 'noname';
+
+            if (isset($part['disposition-filename'])) {
+                $filename = $this->decodeHeader($part['disposition-filename']);
+                // Escape all potentially unsafe characters from the filename
+                $filename = preg_replace('((^\.)|\/|(\.$))', '_', $filename);
+            } elseif (isset($part['content-name'])) {
+                // if we have no disposition but we have a content-name, it's a valid attachment.
+                // we simulate the presence of an attachment disposition with a disposition filename
+                $filename = $this->decodeHeader($part['content-name']);
+                // Escape all potentially unsafe characters from the filename
+                $filename = preg_replace('((^\.)|\/|(\.$))', '_', $filename);
+                $disposition = 'attachment';
+            } elseif (in_array($part['content-type'], $non_attachment_types, true)
+                && $disposition !== 'attachment') {
+                // it is a message body, no attachment
+                continue;
+            } elseif (substr($part['content-type'], 0, 10) !== 'multipart/') {
+                // if we cannot get it by getMessageBody(), we assume it is an attachment
+                $disposition = 'attachment';
+            }
+
+            if (in_array($disposition, $dispositions) === true) {
+                if ($filename == 'noname') {
+                    $nonameIter++;
+                    $filename = 'noname'.$nonameIter;
+                }
+
+                $headersAttachments = $this->getPart('headers', $part);
+                $contentidAttachments = $this->getPart('content-id', $part);
+
+                $mimePartStr = $this->getPartComplete($part);
+
+                $attachments[] = new Attachment(
+                    $filename,
+                    $this->getPart('content-type', $part),
+                    $this->getAttachmentStream($part),
+                    $disposition,
+                    $contentidAttachments,
+                    $headersAttachments,
+                    $mimePartStr
+                );
+            }
+        }
+
+        return $attachments;
+    }
+
+    /**
+     * Save attachments in a folder
+     *
+     * @param string $attach_dir directory
+     * @param bool $include_inline
+     * @param string $filenameStrategy How to generate attachment filenames
+     *
+     * @return array Saved attachments paths
+     * @throws Exception
+     */
+    public function saveAttachments(
+        $attach_dir,
+        $include_inline = true,
+        $filenameStrategy = self::ATTACHMENT_DUPLICATE_SUFFIX
+    ) {
+        $attachments = $this->getAttachments($include_inline);
+        if (empty($attachments)) {
+            return false;
+        }
+
+        if (!is_dir($attach_dir)) {
+            mkdir($attach_dir);
+        }
+
+        $attachments_paths = [];
+        foreach ($attachments as $attachment) {
+            // Determine filename
+            switch ($filenameStrategy) {
+                case self::ATTACHMENT_RANDOM_FILENAME:
+                    $attachment_path = tempnam($attach_dir, '');
+                    break;
+                case self::ATTACHMENT_DUPLICATE_THROW:
+                case self::ATTACHMENT_DUPLICATE_SUFFIX:
+                    $attachment_path = $attach_dir . $attachment->getFilename();
+                    break;
+                default:
+                    throw new Exception('Invalid filename strategy argument provided.');
+            }
+
+            // Handle duplicate filename
+            if (file_exists($attachment_path)) {
+                switch ($filenameStrategy) {
+                    case self::ATTACHMENT_DUPLICATE_THROW:
+                        throw new Exception('Could not create file for attachment: duplicate filename.');
+                    case self::ATTACHMENT_DUPLICATE_SUFFIX:
+                        $attachment_path = tempnam($attach_dir, $attachment->getFilename());
+                        break;
+                }
+            }
+
+            /** @var resource $fp */
+            if ($fp = fopen($attachment_path, 'w')) {
+                while ($bytes = $attachment->read()) {
+                    fwrite($fp, $bytes);
+                }
+                fclose($fp);
+                $attachments_paths[] = realpath($attachment_path);
+            } else {
+                throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.');
+            }
+        }
+
+        return $attachments_paths;
+    }
+
+    /**
+     * Read the attachment Body and save temporary file resource
+     *
+     * @param array $part
+     *
+     * @return resource Mime Body Part
+     * @throws Exception
+     */
+    protected function getAttachmentStream(&$part)
+    {
+        /** @var resource $temp_fp */
+        $temp_fp = tmpfile();
+
+        $headers = $this->getPart('headers', $part);
+        $encodingType = array_key_exists('content-transfer-encoding', $headers) ?
+        $headers['content-transfer-encoding'] : '';
+
+        if ($temp_fp) {
+            if ($this->stream) {
+                $start = $part['starting-pos-body'];
+                $end = $part['ending-pos-body'];
+                fseek($this->stream, $start, SEEK_SET);
+                $len = $end - $start;
+                $written = 0;
+                while ($written < $len) {
+                    $write = $len;
+                    $part = fread($this->stream, $write);
+                    fwrite($temp_fp, $this->decodeContentTransfer($part, $encodingType));
+                    $written += $write;
+                }
+            } elseif ($this->data) {
+                $attachment = $this->decodeContentTransfer($this->getPartBodyFromText($part), $encodingType);
+                fwrite($temp_fp, $attachment, strlen($attachment));
+            }
+            fseek($temp_fp, 0, SEEK_SET);
+        } else {
+            throw new Exception(
+                'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
+            );
+        }
+
+        return $temp_fp;
+    }
+
+    /**
+     * Decode the string from Content-Transfer-Encoding
+     *
+     * @param string $encodedString The string in its original encoded state
+     * @param string $encodingType  The encoding type from the Content-Transfer-Encoding header of the part.
+     *
+     * @return string The decoded string
+     */
+    protected function decodeContentTransfer($encodedString, $encodingType)
+    {
+        $encodingType = strtolower($encodingType);
+        if ($encodingType == 'base64') {
+            return base64_decode($encodedString);
+        } elseif ($encodingType == 'quoted-printable') {
+            return quoted_printable_decode($encodedString);
+        } else {
+            return $encodedString; //8bit, 7bit, binary
+        }
+    }
+
+    /**
+     * $input can be a string or array
+     *
+     * @param string|array $input
+     *
+     * @return string
+     */
+    protected function decodeHeader($input)
+    {
+        //Sometimes we have 2 label From so we take only the first
+        if (is_array($input)) {
+            return $this->decodeSingleHeader($input[0]);
+        }
+
+        return $this->decodeSingleHeader($input);
+    }
+
+    /**
+     * Decodes a single header (= string)
+     *
+     * @param string $input
+     *
+     * @return string
+     */
+    protected function decodeSingleHeader($input)
+    {
+        // For each encoded-word...
+        while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)((\s+)=\?)?/i', $input, $matches)) {
+            $encoded = $matches[1];
+            $charset = $matches[2];
+            $encoding = $matches[3];
+            $text = $matches[4];
+            $space = isset($matches[6]) ? $matches[6] : '';
+
+            switch (strtolower($encoding)) {
+                case 'b':
+                    $text = $this->decodeContentTransfer($text, 'base64');
+                    break;
+
+                case 'q':
+                    $text = str_replace('_', ' ', $text);
+                    preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
+                    foreach ($matches[1] as $value) {
+                        $text = str_replace('='.$value, chr(hexdec($value)), $text);
+                    }
+                    break;
+            }
+
+            $text = $this->charset->decodeCharset($text, $this->charset->getCharsetAlias($charset));
+            $input = str_replace($encoded . $space, $text, $input);
+        }
+
+        return $input;
+    }
+
+    /**
+     * Return the charset of the MIME part
+     *
+     * @param array $part
+     *
+     * @return string|false
+     */
+    protected function getPartCharset($part)
+    {
+        if (isset($part['charset'])) {
+            return $this->charset->getCharsetAlias($part['charset']);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Retrieve a specified MIME part
+     *
+     * @param string $type
+     * @param array  $parts
+     *
+     * @return string|array
+     */
+    protected function getPart($type, $parts)
+    {
+        return (isset($parts[$type])) ? $parts[$type] : false;
+    }
+
+    /**
+     * Retrieve the Body of a MIME part
+     *
+     * @param array $part
+     *
+     * @return string
+     */
+    protected function getPartBody(&$part)
+    {
+        $body = '';
+        if ($this->stream) {
+            $body = $this->getPartBodyFromFile($part);
+        } elseif ($this->data) {
+            $body = $this->getPartBodyFromText($part);
+        }
+
+        return $body;
+    }
+
+    /**
+     * Retrieve the Body from a MIME part from file
+     *
+     * @param array $part
+     *
+     * @return string Mime Body Part
+     */
+    protected function getPartBodyFromFile(&$part)
+    {
+        $start = $part['starting-pos-body'];
+        $end = $part['ending-pos-body'];
+        $body = '';
+        if ($end - $start > 0) {
+            fseek($this->stream, $start, SEEK_SET);
+            $body = fread($this->stream, $end - $start);
+        }
+
+        return $body;
+    }
+
+    /**
+     * Retrieve the Body from a MIME part from text
+     *
+     * @param array $part
+     *
+     * @return string Mime Body Part
+     */
+    protected function getPartBodyFromText(&$part)
+    {
+        $start = $part['starting-pos-body'];
+        $end = $part['ending-pos-body'];
+
+        return substr($this->data, $start, $end - $start);
+    }
+
+    /**
+     * Retrieve the content of a MIME part
+     *
+     * @param array $part
+     *
+     * @return string
+     */
+    protected function getPartComplete(&$part)
+    {
+        $body = '';
+        if ($this->stream) {
+            $body = $this->getPartFromFile($part);
+        } elseif ($this->data) {
+            $body = $this->getPartFromText($part);
+        }
+
+        return $body;
+    }
+
+    /**
+     * Retrieve the content from a MIME part from file
+     *
+     * @param array $part
+     *
+     * @return string Mime Content
+     */
+    protected function getPartFromFile(&$part)
+    {
+        $start = $part['starting-pos'];
+        $end = $part['ending-pos'];
+        $body = '';
+        if ($end - $start > 0) {
+            fseek($this->stream, $start, SEEK_SET);
+            $body = fread($this->stream, $end - $start);
+        }
+
+        return $body;
+    }
+
+    /**
+     * Retrieve the content from a MIME part from text
+     *
+     * @param array $part
+     *
+     * @return string Mime Content
+     */
+    protected function getPartFromText(&$part)
+    {
+        $start = $part['starting-pos'];
+        $end = $part['ending-pos'];
+
+        return substr($this->data, $start, $end - $start);
+    }
+
+    /**
+     * Retrieve the resource
+     *
+     * @return resource resource
+     */
+    public function getResource()
+    {
+        return $this->resource;
+    }
+
+    /**
+     * Retrieve the file pointer to email
+     *
+     * @return resource stream
+     */
+    public function getStream()
+    {
+        return $this->stream;
+    }
+
+    /**
+     * Retrieve the text of an email
+     *
+     * @return string data
+     */
+    public function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * Retrieve the parts of an email
+     *
+     * @return array parts
+     */
+    public function getParts()
+    {
+        return $this->parts;
+    }
+
+    /**
+     * Retrieve the charset manager object
+     *
+     * @return CharsetManager charset
+     */
+    public function getCharset()
+    {
+        return $this->charset;
+    }
+}

+ 22 - 5
data/web/inc/prerequisites.inc.php

@@ -6,8 +6,6 @@ if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php')) {
 }
 $autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
 
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
-
 header_remove("X-Powered-By");
 
 // Yubi OTP API
@@ -49,17 +47,33 @@ try {
 }
 catch (PDOException $e) {
 ?>
-<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/>  <?=$e->getMessage();?></center>
+<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>
 <?php
 exit;
 }
 
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
+
 // Set language
-if (!isset($_SESSION['mailcow_locale'])) {
-  $_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
+if (!isset($_SESSION['mailcow_locale']) && !isset($_COOKIE['mailcow_locale'])) {
+  if ($DETECT_LANGUAGE && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+    $header_lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
+    foreach ($AVAILABLE_LANGUAGES as $available_lang) {
+      if ($header_lang == $available_lang) {
+        $_SESSION['mailcow_locale'] = strtolower(trim($header_lang));
+      }
+    }
+  }
+  else {
+    $_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
+  }
+}
+if (isset($_COOKIE['mailcow_locale'])) {
+  $_SESSION['mailcow_locale'] = $_COOKIE['mailcow_locale'];
 }
 if (isset($_GET['lang']) && in_array($_GET['lang'], $AVAILABLE_LANGUAGES)) {
   $_SESSION['mailcow_locale'] = $_GET['lang'];
+  setcookie("mailcow_locale", $_GET['lang'], time()+30758400); // one year
 }
 
 require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php';
@@ -67,7 +81,9 @@ include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.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.bcc.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantaine.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
@@ -80,3 +96,4 @@ init_db_schema();
 if (isset($_SESSION['mailcow_cc_role'])) {
   set_acl();
 }
+$UI_TEXTS = customize('get', 'ui_texts');

+ 18 - 0
data/web/inc/sessions.inc.php

@@ -26,11 +26,29 @@ if (!isset($_SESSION['SESS_REMOTE_UA'])) {
   $_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT'];
 }
 
+// API
+if (!empty($_SERVER['HTTP_X_API_KEY'])) {
+  $stmt = $pdo->prepare("SELECT `username`, `allow_from` FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
+  $stmt->execute(array(
+    ':api_key' => preg_replace('/[^A-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
+  ));
+  $api_return = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (!empty($api_return['username'])) {
+    if (in_array($_SERVER['REMOTE_ADDR'], explode(',', $api_return['allow_from']))) {
+      $_SESSION['mailcow_cc_username'] = $api_return['username'];
+      $_SESSION['mailcow_cc_role'] = 'admin';
+      $_SESSION['mailcow_cc_api'] = true;
+    }
+  }
+}
 // Update session cookie
 // setcookie(session_name() ,session_id(), time() + $SESSION_LIFETIME);
 
 // Check session
 function session_check() {
+  if ($_SESSION['mailcow_cc_api'] === true) {
+    return true;
+  }
   if (!isset($_SESSION['SESS_REMOTE_UA'])) {
     return false;
   }

+ 12 - 0
data/web/inc/triggers.inc.php

@@ -64,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
 	}
 }
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
+  // TODO: Move file upload to API?
 	if (isset($_POST["submit_main_logo"])) {
     if ($_FILES['main_logo']['error'] == 0) {
       customize('add', 'main_logo', $_FILES);
@@ -72,5 +73,16 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
 	if (isset($_POST["reset_main_logo"])) {
     customize('delete', 'main_logo');
 	}
+  // API cannot be controlled by API
+	if (isset($_POST["admin_api"])) {
+		admin_api('edit', $_POST);
+	}
+	if (isset($_POST["admin_api_regen_key"])) {
+		admin_api('regen_key', $_POST);
+	}
+  // Not available via API
+	if (isset($_POST["rspamd_ui"])) {
+		rspamd_ui('edit', $_POST);
+	}
 }
 ?>

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

@@ -68,8 +68,12 @@ $autodiscover_config = array(
 );
 unset($https_port);
 
+// If false, we will use DEFAULT_LANG
+// Uses HTTP_ACCEPT_LANGUAGE header
+$DETECT_LANGUAGE = true;
+
 // Change default language, "de", "en", "es", "nl", "pt", "ru"
-$DEFAULT_LANG = 'en';
+$DEFAULT_LANG = 'de';
 
 // Available languages
 $AVAILABLE_LANGUAGES = array('de', 'en', 'es', 'fr', 'nl', 'pl', 'pt', 'ru', 'it');
@@ -92,13 +96,7 @@ $MAILCOW_APPS = array(
   array(
     'name' => 'SOGo',
     'link' => '/SOGo/',
-    'description' => 'SOGo is a web-based client for email, address book and calendar.'
-  ),
-  // array(
-    // 'name' => 'Roundcube',
-    // 'link' => '/rc/',
-    // 'description' => 'Roundcube is a web-based email client.',
-  // ),
+  )
 );
 
 // Rows until pagination begins
@@ -118,3 +116,7 @@ $OTP_LABEL = "mailcow UI";
 
 // Default "to" address in relay test tool
 $RELAY_TO = "null@hosted.mailcow.de";
+
+// Quarantaine data age in days to keep
+$QUARANTAINE_AGE = 10;
+

+ 17 - 11
data/web/index.php

@@ -13,9 +13,9 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   header('Location: /user.php');
   exit();
 }
-
 require_once 'inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+
 ?>
 <div class="container">
   <div class="row">
@@ -24,7 +24,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
         <div class="panel-heading"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?= $lang['login']['login']; ?></div>
         <div class="panel-body">
           <div class="text-center mailcow-logo"><img src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>" alt="mailcow"></div>
-          <legend>mailcow UI</legend>
+          <legend><?=$UI_TEXTS['main_name'];?></legend>
             <form method="post" autofill="off">
             <div class="form-group">
               <label class="sr-only" for="login_user"><?= $lang['login']['username']; ?></label>
@@ -65,7 +65,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
             <?php
             endif;
             ?>
-          <legend>mailcow Apps</legend>
+          <legend><?=$UI_TEXTS['apps_name'];?></legend>
           <?php
           foreach ($MAILCOW_APPS as $app):
           ?>
@@ -73,12 +73,14 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
           <?php
           endforeach;
           $app_links = customize('get', 'app_links');
-          foreach ($app_links as $row) {
-            foreach ($row as $key => $val):
-          ?>
-            <a href="<?= htmlspecialchars($val); ?>" role="button" class="btn btn-lg btn-default"><?= htmlspecialchars($key); ?></a>&nbsp;
-          <?php 
-            endforeach;
+          if (!empty($app_links)) {
+            foreach ($app_links as $row) {
+              foreach ($row as $key => $val):
+            ?>
+              <a href="<?= htmlspecialchars($val); ?>" role="button" class="btn btn-lg btn-default"><?= htmlspecialchars($key); ?></a>&nbsp;
+            <?php 
+              endforeach;
+            }
           }
           ?>
         </div>
@@ -91,10 +93,14 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
         </div>
         <div id="collapse1" class="panel-collapse collapse">
           <div class="panel-body">
-            <p><span style="border-bottom: 1px dotted #999;">mailcow UI</span></p>
+            <?php if ($UI_TEXTS['help_text']): ?>
+            <p><?=$UI_TEXTS['help_text'];?></p>
+            <?php else: ?>
+            <p><span style="border-bottom: 1px dotted #999;"><?=$UI_TEXTS['main_name'];?></span></p>
             <p><?= $lang['start']['mailcow_panel_detail']; ?></p>
-            <p><span style="border-bottom: 1px dotted #999;">mailcow Apps</span></p>
+            <p><span style="border-bottom: 1px dotted #999;"><?=$UI_TEXTS['apps_name'];?></span></p>
             <p><?= $lang['start']['mailcow_apps_detail']; ?></p>
+            <?php endif; ?>
           </div>
         </div>
       </div>

+ 1 - 349
data/web/js/admin.js

@@ -5,199 +5,10 @@ jQuery(function($){
   var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
   function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
   function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
-  $("#refresh_postfix_log").on('click', function(e) {
-    e.preventDefault();
-    draw_postfix_logs();
-  });
-  $("#refresh_autodiscover_log").on('click', function(e) {
-    e.preventDefault();
-    draw_autodiscover_logs();
-  });
-  $("#refresh_dovecot_log").on('click', function(e) {
-    e.preventDefault();
-    draw_dovecot_logs();
-  });
-  $("#refresh_sogo_log").on('click', function(e) {
-    e.preventDefault();
-    draw_sogo_logs();
-  });
-  $("#refresh_fail2ban_log").on('click', function(e) {
-    e.preventDefault();
-    draw_fail2ban_logs();
-  });
-  $("#refresh_rspamd_history").on('click', function(e) {
-    e.preventDefault();
-    draw_rspamd_history();
-  });
   $("#import_dkim_legend").on('click', function(e) {
     e.preventDefault();
     $('#import_dkim_arrow').toggleClass("animation"); 
   });
-  function draw_autodiscover_logs() {
-    ft_autodiscover_logs = FooTable.init('#autodiscover_log', {
-      "columns": [
-        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name":"ua","title":"User-Agent","style":{"min-width":"200px"}},
-        {"name":"user","title":"Username","style":{"min-width":"200px"}},
-        {"name":"service","title":"Service"},
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/autodiscover/100',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw autodiscover log table');
-        },
-        success: function (data) {
-          return process_table_data(data, 'autodiscover_log');
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
-      "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
-      "sorting": {"enabled": true},
-      "on": {"ready.ft.table": function(e, ft){
-          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-          $(heading).children('.log-lines').text(function(){
-            var ft_paging = ft.use(FooTable.Paging)
-            return ft_paging.totalRows;
-          })
-        }
-      }
-    });
-  }
-  function draw_postfix_logs() {
-    ft_postfix_logs = FooTable.init('#postfix_log', {
-      "columns": [
-        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
-        {"name":"message","title":lang.message},
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/postfix',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw postfix log table');
-        },
-        success: function (data) {
-          return process_table_data(data, 'general_syslog');
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
-      "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
-      "sorting": {"enabled": true},
-      "on": {
-        "ready.ft.table": function(e, ft){
-          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-          $(heading).children('.log-lines').text(function(){
-            var ft_paging = ft.use(FooTable.Paging)
-            return ft_paging.totalRows;
-          })
-        }
-      }
-    });
-  }
-  function draw_fail2ban_logs() {
-    ft_fail2ban_logs = FooTable.init('#fail2ban_log', {
-      "columns": [
-        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
-        {"name":"message","title":lang.message},
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/fail2ban',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw fail2ban log table');
-        },
-        success: function (data) {
-          return process_table_data(data, 'general_syslog');
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
-      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
-      "sorting": {"enabled": true},
-      "on": {
-        "ready.ft.table": function(e, ft){
-          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-          $(heading).children('.log-lines').text(function(){
-            var ft_paging = ft.use(FooTable.Paging)
-            return ft_paging.totalRows;
-          })
-        }
-      }
-    });
-  }
-  function draw_sogo_logs() {
-    ft_sogo_logs = FooTable.init('#sogo_log', {
-      "columns": [
-        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
-        {"name":"message","title":lang.message},
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/sogo',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw sogo log table');
-        },
-        success: function (data) {
-          return process_table_data(data, 'general_syslog');
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
-      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
-      "sorting": {"enabled": true},
-      "on": {
-        "ready.ft.table": function(e, ft){
-          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-          $(heading).children('.log-lines').text(function(){
-            var ft_paging = ft.use(FooTable.Paging)
-            return ft_paging.totalRows;
-          })
-        }
-      }
-    });
-  }
-  function draw_dovecot_logs() {
-    ft_dovecot_logs = FooTable.init('#dovecot_log', {
-      "columns": [
-        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
-        {"name":"message","title":lang.message},
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/dovecot',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw dovecot log table');
-        },
-        success: function (data) {
-          return process_table_data(data, 'general_syslog');
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
-      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
-      "sorting": {"enabled": true},
-      "on": {
-        "ready.ft.table": function(e, ft){
-          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-          $(heading).children('.log-lines').text(function(){
-            var ft_paging = ft.use(FooTable.Paging)
-            return ft_paging.totalRows;
-          })
-        }
-      }
-    });
-  }
   function draw_domain_admins() {
     ft_domainadmins = FooTable.init('#domainadminstable', {
       "columns": [
@@ -278,108 +89,9 @@ jQuery(function($){
       "sorting": {"enabled": true}
     });
   }
-  function draw_rspamd_history() {
-    ft_rspamd_history = FooTable.init('#rspamd_history', {
-      "columns": [
-        {"name":"unix_time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name": "ip","title": "IP address","breakpoints": "all","style": {"minWidth": 88}},
-        {"name": "sender_mime","title": "From","breakpoints": "xs sm md","style": {"minWidth": 100}},
-        {"name": "rcpt_mime","title": "To","breakpoints": "xs sm md","style": {"minWidth": 100}},
-        {"name": "subject","title": "Subject","breakpoints": "all","style": {"word-break": "break-all","minWidth": 150}},
-        {"name": "action","title": "Action","style": {"minwidth": 82}},
-        {"name": "score","title": "Score","style": {"maxWidth": 110},},
-        {"name": "symbols","title": "Symbols","breakpoints": "all",},
-        {"name": "size","title": "Msg size","breakpoints": "all","style": {"minwidth": 50},"formatter": function(value){return humanFileSize(value);}},
-        {"name": "scan_time","title": "Scan time","breakpoints": "all","style": {"maxWidth": 72},},
-        {"name": "message-id","title": "ID","breakpoints": "all","style": {"minWidth": 130,"overflow": "hidden","textOverflow": "ellipsis","wordBreak": "break-all","whiteSpace": "normal"}},
-        {"name": "user","title": "Authenticated user","breakpoints": "xs sm md","style": {"minWidth": 100}}
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/rspamd-history',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw rspamd history table');
-        },
-        success: function (data) {
-          return process_table_data(data, 'rspamd_history');
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
-      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
-      "sorting": {"enabled": true},
-      "on": {
-        "ready.ft.table": function(e, ft){
-          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-          $(heading).children('.log-lines').text(function(){
-            var ft_paging = ft.use(FooTable.Paging)
-            return ft_paging.totalRows;
-          })
-        }
-      }
-    });
-  }
 
   function process_table_data(data, table) {
-    if (table == 'rspamd_history') {
-    $.each(data, function (i, item) {
-      item.rcpt_mime = item.rcpt_mime.join(",&#8203;");
-      Object.keys(item.symbols).map(function(key) {
-        var sym = item.symbols[key];
-        if (sym.score <= 0) {
-          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
-        }
-        else {
-          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
-        }
-        var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
-        if (sym.options) {
-          str += ' [' + sym.options.join(",") + "]";
-        }
-        item.symbols[key].str = str;
-      });
-      item.symbols = Object.keys(item.symbols).
-      map(function(key) {
-        return item.symbols[key];
-      }).sort(function(e1, e2) {
-        return Math.abs(e1.score) < Math.abs(e2.score);
-      }).map(function(e) {
-        return e.str;
-      }).join("<br>\n");
-      var scan_time = item.time_real.toFixed(3) + ' / ' + item.time_virtual.toFixed(3);
-      item.scan_time = {
-        "options": {
-          "sortValue": item.time_real
-        },
-        "value": scan_time
-      };
-      if (item.action === 'clean' || item.action === 'no action') {
-        item.action = "<div class='label label-success'>" + item.action + "</div>";
-      } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
-        item.action = "<div class='label label-warning'>" + item.action + "</div>";
-      } else if (item.action === 'spam' || item.action === 'reject') {
-        item.action = "<div class='label label-danger'>" + item.action + "</div>";
-      } else {
-        item.action = "<div class='label label-info'>" + item.action + "</div>";
-      }
-      var score_content;
-      if (item.score < item.required_score) {
-        score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
-      } else {
-        score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
-      }
-      item.score = {
-        "options": {
-          "sortValue": item.score
-        },
-        "value": score_content
-      };
-      if (item.user == null) {
-        item.user = "none";
-      }
-    });
-    } else if (table == 'relayhoststable') {
+    if (table == 'relayhoststable') {
       $.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>' +
@@ -409,48 +121,13 @@ jQuery(function($){
           '<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>' +
           '</div>';
       });
-    } else if (table == 'autodiscover_log') {
-      $.each(data, function (i, item) {
-        item.ua = '<span style="font-size:small">' + item.ua + '</span>';
-        if (item.service == "activesync") {
-          item.service = '<span class="label label-info">ActiveSync</span>';
-        }
-        else if (item.service == "imap") {
-          item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>';
-        }
-        else {
-          item.service = '<span class="label label-danger">' + item.service + '</span>';
-        }
-      });
-    } else if (table == 'general_syslog') {
-      $.each(data, function (i, item) {
-        item.message = escapeHtml(item.message);
-        var danger_class = ["emerg", "alert", "crit", "err"];
-        var warning_class = ["warning", "warn"];
-        var info_class = ["notice", "info", "debug"];
-        if (jQuery.inArray(item.priority, danger_class) !== -1) {
-          item.priority = '<span class="label label-danger">' + item.priority + '</span>';
-        } 
-        else if (jQuery.inArray(item.priority, warning_class) !== -1) {
-          item.priority = '<span class="label label-warning">' + item.priority + '</span>';
-        }
-        else if (jQuery.inArray(item.priority, info_class) !== -1) {
-          item.priority = '<span class="label label-info">' + item.priority + '</span>';
-        }
-      });
     }
     return data
   };
   // Initial table drawings
-  draw_postfix_logs();
-  draw_autodiscover_logs();
-  draw_dovecot_logs();
-  draw_sogo_logs();
-  draw_fail2ban_logs();
   draw_domain_admins();
   draw_fwd_hosts();
   draw_relayhosts();
-  draw_rspamd_history();
   // Relayhost
   $('#testRelayhostModal').on('show.bs.modal', function (e) {
     $('#test_relayhost_result').text("-");
@@ -485,31 +162,6 @@ jQuery(function($){
       $('#priv_key_pre').text(decoded_key);
     }
   })
-
-  $('.add_log_lines').on('click', function (e) {
-    e.preventDefault();
-    var log_table= $(this).data("table")
-    var new_nrows = ($(this).data("nrows") - 1)
-    var post_process = $(this).data("post-process")
-    var log_url = $(this).data("log-url")
-    if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
-      console.log("no data-table or data-nrows or log_url or data-post-process attr found");
-      return;
-    }
-    if (ft = FooTable.get($('#' + log_table))) {
-      var heading = ft.$el.parents('.tab-pane').find('.panel-heading')
-      var ft_paging = ft.use(FooTable.Paging)
-      var load_rows = ft_paging.totalRows + '-' + (ft_paging.totalRows + new_nrows)
-      $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
-        if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
-        var rows = process_table_data(data, post_process);
-        var rows_now = (ft_paging.totalRows + data.length);
-        $(heading).children('.log-lines').text(rows_now)
-        mailcow_alert_box(data.length + lang.additional_rows, "success");
-        ft.rows.load(rows, true);
-      });
-    }
-  })
   // App links
   function add_table_row(table_id) {
     var row = $('<tr />');

+ 41 - 9
data/web/js/api.js

@@ -1,4 +1,15 @@
 $(document).ready(function() {
+  function is_active(elem) {
+    if ($(elem).data('submitted') == '1') {
+      return true;
+    } else {
+      $(elem).text(loading_text);
+      $(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);
+      return false;
+    }
+  }
   $.fn.serializeObject = function() {
     var o = {};
     var a = this.serializeArray();
@@ -80,12 +91,12 @@ $(document).ready(function() {
           }
         }
         if ($(this).attr("max")) {
-          if ($(this).val() > $(this).attr("max")) {
+          if (Number($(this).val()) > Number($(this).attr("max"))) {
             invalid = true;
             $(this).addClass('inputMissingAttr');
           } else {
             if ($(this).attr("min")) {
-              if ($(this).val() < $(this).attr("min")) {
+              if (Number($(this).val()) < Number($(this).attr("min"))) {
                 invalid = true;
                 $(this).addClass('inputMissingAttr');
               } else {
@@ -102,6 +113,7 @@ $(document).ready(function() {
         return false;
       }
     }
+    // alert(JSON.stringify(api_attr));
     // If clicked element #edit_selected has data-item attribute, it is added to "items"
     if (typeof $(this).data('item') !== 'undefined') {
       var id = $(this).data('id');
@@ -114,6 +126,7 @@ $(document).ready(function() {
     api_items = multi_data[id];
     // alert(JSON.stringify(api_attr));
     if (Object.keys(api_items).length !== 0) {
+      if (is_active($(this))) { return false; }
       $.ajax({
         type: "POST",
         dataType: "json",
@@ -126,7 +139,9 @@ $(document).ready(function() {
         jsonp: false,
         complete: function(data) {
           var response = (data.responseText);
-          response_obj = JSON.parse(response);
+          if (typeof response !== 'undefined' && response.length !== 0) {
+            response_obj = JSON.parse(response);
+          }
           if (api_reload_window === true) {
             window.location = window.location.href.split("#")[0];
           }
@@ -141,6 +156,11 @@ $(document).ready(function() {
     var id = $(this).data('id');
     var api_url = $(this).data('api-url');
     var api_attr = $(this).data('api-attr');
+    if (typeof $(this).data('api-reload-window') !== 'undefined') {
+      api_reload_window = $(this).data('api-reload-window');
+    } else {
+      api_reload_window = true;
+    }
     // If clicked button is in a form with the same data-id as the button,
     // we merge all input fields by {"name":"value"} into api-attr
     if ($(this).closest("form").data('id') == id) {
@@ -155,12 +175,13 @@ $(document).ready(function() {
           }
         }
         if ($(this).attr("max")) {
-          if ($(this).val() > $(this).attr("max")) {
+          if (Number($(this).val()) > Number($(this).attr("max"))) {
+            alert($(this).attr("max"))
             invalid = true;
             $(this).addClass('inputMissingAttr');
           } else {
             if ($(this).attr("min")) {
-              if ($(this).val() < $(this).attr("min")) {
+              if (Number($(this).val()) < Number($(this).attr("min"))) {
                 invalid = true;
                 $(this).addClass('inputMissingAttr');
               } else {
@@ -177,6 +198,7 @@ $(document).ready(function() {
         return false;
       }
     }
+    if (is_active($(this))) { return false; }
     // alert(JSON.stringify(api_attr));
     $.ajax({
       type: "POST",
@@ -188,10 +210,20 @@ $(document).ready(function() {
       url: '/api/v1/' + api_url,
       jsonp: false,
       complete: function(data) {
-        // var reponse = (JSON.parse(data.responseText));
-        // console.log(reponse.type);
-        // console.log(reponse.msg);
-        window.location = window.location.href.split("#")[0];
+        var response = (data.responseText);
+        if (typeof response !== 'undefined' && response.length !== 0) {
+          response_obj = JSON.parse(response);
+          if (response_obj.type == 'success') {
+            $('form').formcache('clear');
+          }
+          else {
+            var add_modal = $('.modal.in').attr('id');
+            localStorage.setItem("add_modal", add_modal);
+          }
+        }
+        if (api_reload_window === true) {
+          window.location = window.location.href.split("#")[0];
+        }
       }
     });
   });

+ 503 - 0
data/web/js/debug.js

@@ -0,0 +1,503 @@
+jQuery(function($){
+  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
+  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
+  $("#refresh_postfix_log").on('click', function(e) {
+    e.preventDefault();
+    draw_postfix_logs();
+  });
+  $("#refresh_autodiscover_log").on('click', function(e) {
+    e.preventDefault();
+    draw_autodiscover_logs();
+  });
+  $("#refresh_dovecot_log").on('click', function(e) {
+    e.preventDefault();
+    draw_dovecot_logs();
+  });
+  $("#refresh_sogo_log").on('click', function(e) {
+    e.preventDefault();
+    draw_sogo_logs();
+  });
+  $("#refresh_watchdog_log").on('click', function(e) {
+    e.preventDefault();
+    draw_watchdog_logs();
+  });
+  $("#refresh_api_log").on('click', function(e) {
+    e.preventDefault();
+    draw_api_logs();
+  });
+  $("#refresh_acme_log").on('click', function(e) {
+    e.preventDefault();
+    draw_acme_logs();
+  });
+  $("#refresh_fail2ban_log").on('click', function(e) {
+    e.preventDefault();
+    draw_fail2ban_logs();
+  });
+  $("#refresh_rspamd_history").on('click', function(e) {
+    e.preventDefault();
+    draw_rspamd_history();
+  });
+  function draw_autodiscover_logs() {
+    ft_autodiscover_logs = FooTable.init('#autodiscover_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"ua","title":"User-Agent","style":{"min-width":"200px"}},
+        {"name":"user","title":"Username","style":{"min-width":"200px"}},
+        {"name":"service","title":"Service"},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/autodiscover/100',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw autodiscover log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'autodiscover_log');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {"ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_postfix_logs() {
+    ft_postfix_logs = FooTable.init('#postfix_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/postfix',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw postfix log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'general_syslog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_watchdog_logs() {
+    ft_watchdog_logs = FooTable.init('#watchdog_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"service","title":"Service"},
+        {"name":"trend","title":"Trend"},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/watchdog',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw watchdog log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'watchdog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_api_logs() {
+    ft_api_logs = FooTable.init('#api_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"uri","title":"URI","style":{"width":"310px"}},
+        {"name":"method","title":"Method","style":{"width":"80px"}},
+        {"name":"remote","title":"IP","style":{"width":"80px"}},
+        {"name":"data","title":"Data","style":{"word-break":"break-all"}},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/api',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw api log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'apilog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_acme_logs() {
+    ft_acme_logs = FooTable.init('#acme_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/acme',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw acme log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'general_syslog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_fail2ban_logs() {
+    ft_fail2ban_logs = FooTable.init('#fail2ban_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/fail2ban',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw fail2ban log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'general_syslog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_sogo_logs() {
+    ft_sogo_logs = FooTable.init('#sogo_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/sogo',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw sogo log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'general_syslog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_dovecot_logs() {
+    ft_dovecot_logs = FooTable.init('#dovecot_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/dovecot',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw dovecot log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'general_syslog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_rspamd_history() {
+    ft_rspamd_history = FooTable.init('#rspamd_history', {
+      "columns": [
+        {"name":"unix_time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name": "ip","title": "IP address","breakpoints": "all","style": {"minWidth": 88}},
+        {"name": "sender_mime","title": "From","breakpoints": "xs sm md","style": {"minWidth": 100}},
+        {"name": "rcpt_mime","title": "To","breakpoints": "xs sm md","style": {"minWidth": 100}},
+        {"name": "subject","title": "Subject","breakpoints": "all","style": {"word-break": "break-all","minWidth": 150}},
+        {"name": "action","title": "Action","style": {"minwidth": 82}},
+        {"name": "score","title": "Score","style": {"maxWidth": 110},},
+        {"name": "symbols","title": "Symbols","breakpoints": "all",},
+        {"name": "size","title": "Msg size","breakpoints": "all","style": {"minwidth": 50},"formatter": function(value){return humanFileSize(value);}},
+        {"name": "scan_time","title": "Scan time","breakpoints": "all","style": {"maxWidth": 72},},
+        {"name": "message-id","title": "ID","breakpoints": "all","style": {"minWidth": 130,"overflow": "hidden","textOverflow": "ellipsis","wordBreak": "break-all","whiteSpace": "normal"}},
+        {"name": "user","title": "Authenticated user","breakpoints": "xs sm md","style": {"minWidth": 100}}
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/rspamd-history',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw rspamd history table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'rspamd_history');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+
+  function process_table_data(data, table) {
+    if (table == 'rspamd_history') {
+    $.each(data, function (i, item) {
+      item.rcpt_mime = item.rcpt_mime.join(",&#8203;");
+      Object.keys(item.symbols).map(function(key) {
+        var sym = item.symbols[key];
+        if (sym.score <= 0) {
+          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
+        }
+        else {
+          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
+        }
+        var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
+        if (sym.options) {
+          str += ' [' + sym.options.join(",") + "]";
+        }
+        item.symbols[key].str = str;
+      });
+      item.symbols = Object.keys(item.symbols).
+      map(function(key) {
+        return item.symbols[key];
+      }).sort(function(e1, e2) {
+        return Math.abs(e1.score) < Math.abs(e2.score);
+      }).map(function(e) {
+        return e.str;
+      }).join("<br>\n");
+      var scan_time = item.time_real.toFixed(3) + ' / ' + item.time_virtual.toFixed(3);
+      item.scan_time = {
+        "options": {
+          "sortValue": item.time_real
+        },
+        "value": scan_time
+      };
+      if (item.action === 'clean' || item.action === 'no action') {
+        item.action = "<div class='label label-success'>" + item.action + "</div>";
+      } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
+        item.action = "<div class='label label-warning'>" + item.action + "</div>";
+      } else if (item.action === 'spam' || item.action === 'reject') {
+        item.action = "<div class='label label-danger'>" + item.action + "</div>";
+      } else {
+        item.action = "<div class='label label-info'>" + item.action + "</div>";
+      }
+      var score_content;
+      if (item.score < item.required_score) {
+        score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+      } else {
+        score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+      }
+      item.score = {
+        "options": {
+          "sortValue": item.score
+        },
+        "value": score_content
+      };
+      if (item.user == null) {
+        item.user = "none";
+      }
+    });
+    } else if (table == 'autodiscover_log') {
+      $.each(data, function (i, item) {
+        item.ua = '<span style="font-size:small">' + item.ua + '</span>';
+        if (item.service == "activesync") {
+          item.service = '<span class="label label-info">ActiveSync</span>';
+        }
+        else if (item.service == "imap") {
+          item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>';
+        }
+        else {
+          item.service = '<span class="label label-danger">' + item.service + '</span>';
+        }
+      });
+    } else if (table == 'watchdog') {
+      $.each(data, function (i, item) {
+        if (item.message == null) {
+          item.message = 'Health level: ' + item.lvl + '% (' + item.hpnow + '/' + item.hptotal + ')';
+          if (item.hpdiff < 0) {
+            item.trend = '<span class="label label-danger"><span class="glyphicon glyphicon-arrow-down"></span> ' + item.hpdiff + '</span>';
+          }
+          else if (item.hpdiff == 0) {
+            item.trend = '<span class="label label-info"><span class="glyphicon glyphicon-arrow-right"></span> ' + item.hpdiff + '</span>';
+          }
+          else {
+            item.trend = '<span class="label label-success"><span class="glyphicon glyphicon-arrow-up"></span> ' + item.hpdiff + '</span>';
+          }
+        }
+        else {
+          item.trend = '';
+          item.service = '';
+        }
+      });
+    } else if (table == 'general_syslog') {
+      $.each(data, function (i, item) {
+        if (item === null) { return true; }
+        item.message = escapeHtml(item.message);
+        var danger_class = ["emerg", "alert", "crit", "err"];
+        var warning_class = ["warning", "warn"];
+        var info_class = ["notice", "info", "debug"];
+        if (jQuery.inArray(item.priority, danger_class) !== -1) {
+          item.priority = '<span class="label label-danger">' + item.priority + '</span>';
+        } else if (jQuery.inArray(item.priority, warning_class) !== -1) {
+          item.priority = '<span class="label label-warning">' + item.priority + '</span>';
+        } else if (jQuery.inArray(item.priority, info_class) !== -1) {
+          item.priority = '<span class="label label-info">' + item.priority + '</span>';
+        }
+      });
+    } else if (table == 'apilog') {
+      $.each(data, function (i, item) {
+        if (item === null) { return true; }
+        if (item.method == 'GET') {
+          item.method = '<span class="label label-success">' + item.method + '</span>';
+        } else if (item.method == 'POST') {
+          item.method = '<span class="label label-warning">' + item.method + '</span>';
+        }
+      });
+    }
+    return data
+  };
+  $('.add_log_lines').on('click', function (e) {
+    e.preventDefault();
+    var log_table= $(this).data("table")
+    var new_nrows = ($(this).data("nrows") - 1)
+    var post_process = $(this).data("post-process")
+    var log_url = $(this).data("log-url")
+    if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
+      console.log("no data-table or data-nrows or log_url or data-post-process attr found");
+      return;
+    }
+    if (ft = FooTable.get($('#' + log_table))) {
+      var heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+      var ft_paging = ft.use(FooTable.Paging)
+      var load_rows = ft_paging.totalRows + '-' + (ft_paging.totalRows + new_nrows)
+      $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
+        if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
+        var rows = process_table_data(data, post_process);
+        var rows_now = (ft_paging.totalRows + data.length);
+        $(heading).children('.log-lines').text(rows_now)
+        mailcow_alert_box(data.length + lang.additional_rows, "success");
+        ft.rows.load(rows, true);
+      });
+    }
+  })
+  // Initial table drawings
+  draw_postfix_logs();
+  draw_autodiscover_logs();
+  draw_dovecot_logs();
+  draw_sogo_logs();
+  draw_watchdog_logs();
+  draw_acme_logs();
+  draw_api_logs();
+  draw_fail2ban_logs();
+  draw_rspamd_history();
+
+});

File diff suppressed because it is too large
+ 9 - 0
data/web/js/formcache.min.js


+ 90 - 11
data/web/js/mailbox.js

@@ -55,6 +55,23 @@ $(document).ready(function() {
     });
   });
 
+  // Log modal
+  $('#dnsInfoModal').on('show.bs.modal', function(e) {
+    var domain = $(e.relatedTarget).data('domain');
+    $('.dns-modal-body').html('<center><span style="font-size:18pt;margin:50px" class="glyphicon glyphicon-refresh glyphicon-spin"></span></center>');
+    $.ajax({
+      url: '/inc/ajax/dns_diagnostics.php',
+      data: { domain: domain },
+      dataType: 'text',
+      success: function(data){
+        $('.dns-modal-body').html(data);
+      },
+      error: function(xhr, status, error) {
+        $('.dns-modal-body').html(xhr.responseText);
+      }
+    });
+  });
+
   // Sieve data modal
   $('#sieveDataModal').on('show.bs.modal', function(e) {
     var sieveScript = $(e.relatedTarget).data('sieve-script');
@@ -154,7 +171,7 @@ jQuery(function($){
         {"name":"max_quota_for_mbox","title":lang.mailbox_quota,"breakpoints":"xs sm"},
         {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm"},
         {"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":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"}
       ],
       "rows": $.ajax({
         dataType: 'json',
@@ -170,17 +187,15 @@ jQuery(function($){
             item.quota = item.quota_used_in_domain + "/" + item.max_quota_for_domain;
             item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox);
             item.chkbox = '<input type="checkbox" data-id="domain" name="multi_select" value="' + item.domain_name + '" />';
+            item.action = '<div class="btn-group">';
             if (role == "admin") {
-              item.action = '<div class="btn-group">' +
-                '<a href="/edit.php?domain=' + encodeURI(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="' + encodeURI(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
-                '</div>';
+              item.action += '<a href="/edit.php?domain=' + encodeURI(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="' + encodeURI(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>';
             }
             else {
-              item.action = '<div class="btn-group">' +
-                '<a href="/edit.php?domain=' + encodeURI(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-                '</div>';
+              item.action += '<a href="/edit.php?domain=' + encodeURI(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="' + encodeURI(item.domain_name) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>';
           });
         }
       }),
@@ -193,6 +208,7 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -263,6 +279,7 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -307,6 +324,58 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
+        "placeholder": lang.filter_table
+      },
+      "sorting": {
+        "enabled": true
+      }
+    });
+  }
+  function draw_bcc_table() {
+    ft_bcc_table = FooTable.init('#bcc_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":"type","title":lang.bcc_type},
+        {"name":"local_dest","title":lang.bcc_local_dest},
+        {"name":"bcc_dest","title":lang.bcc_destinations},
+        {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
+        {"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":lang.action,"breakpoints":"xs sm"}
+      ],
+      "empty": lang.empty,
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/bcc/all',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw bcc table');
+        },
+        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>' +
+              '</div>';
+            item.chkbox = '<input type="checkbox" data-id="bcc" name="multi_select" value="' + item.id + '" />';
+            if (item.type == 'sender') {
+              item.type = '<span id="active-script" class="label label-success">Sender</span>';
+            } else {
+              item.type = '<span id="inactive-script" class="label label-warning">Recipient</span>';
+            }
+          });
+        }
+      }),
+      "paging": {
+        "enabled": true,
+        "limit": 5,
+        "size": pagination_size
+      },
+      "filtering": {
+        "enabled": true,
+        "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -314,7 +383,6 @@ jQuery(function($){
       }
     });
   }
-
   function draw_alias_table() {
     ft_alias_table = FooTable.init('#alias_table', {
       "columns": [
@@ -360,6 +428,7 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -375,7 +444,7 @@ jQuery(function($){
         {"sorted": true,"name":"alias_domain","title":lang.alias,"style":{"width":"250px"}},
         {"name":"target_domain","title":lang.target_domain},
         {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
-        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"250px","width":"250px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
       ],
       "empty": lang.empty,
       "rows": $.ajax({
@@ -390,6 +459,7 @@ jQuery(function($){
             item.action = '<div class="btn-group">' +
               '<a href="/edit.php?aliasdomain=' + encodeURI(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="' + encodeURI(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="' + encodeURI(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="' + item.alias_domain + '" />';
           });
@@ -403,6 +473,7 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -418,6 +489,7 @@ jQuery(function($){
         {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
         {"name":"user2","title":lang.owner},
         {"name":"server_w_port","title":"Server","breakpoints":"xs"},
+        {"name":"exclude","title":lang.excludes,"breakpoints":"all"},
         {"name":"mins_interval","title":lang.mins_interval,"breakpoints":"all"},
         {"name":"last_run","title":lang.last_run,"breakpoints":"all"},
         {"name":"log","title":"Log"},
@@ -436,7 +508,11 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURI(item.id) + '">Open logs</a>'
-            item.exclude = '<code>' + item.exclude + '</code>'
+            if (!item.exclude > 0) {
+              item.exclude = '-';
+            } else {
+              item.exclude  = '<code>' + item.exclude + '</code>';
+            }
             item.server_w_port = 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>' +
@@ -462,6 +538,7 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -515,6 +592,7 @@ jQuery(function($){
       "filtering": {
         "enabled": true,
         "position": "left",
+        "connectors": false,
         "placeholder": lang.filter_table
       },
       "sorting": {
@@ -530,5 +608,6 @@ jQuery(function($){
   draw_aliasdomain_table();
   draw_sync_job_table();
   draw_filter_table();
+  draw_bcc_table();
 
 });

+ 82 - 0
data/web/js/quarantaine.js

@@ -0,0 +1,82 @@
+// Base64 functions
+var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
+jQuery(function($){
+  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
+  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
+
+  function draw_quarantaine_table() {
+    ft_quarantainetable = FooTable.init('#quarantainetable', {
+      "columns": [
+        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"id","type":"ID","filterable": false,"sorted": true,"direction":"DESC","title":"ID","style":{"width":"50px"}},
+        {"name":"qid","type":"text","title":lang.qid,"style":{"width":"125px"}},
+        {"name":"sender","title":lang.sender,"breakpoints":"xs sm"},
+        {"name":"rcpt","title":lang.rcpt, "type": "text"},
+        {"name":"created","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.received,"style":{"width":"170px"}},
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right"},"style":{"width":"205px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/quarantaine/all',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw quarantaine table');
+        },
+        success: function (data) {
+          $.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>' +
+              '</div>';
+            item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
+          });
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": pagination_size},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(ev, ft){
+          $('.show_qid_info').on('click', function (e) {
+            e.preventDefault();
+            var qitem = $(this).data('item');
+            $('#qidDetailModal').modal('show');
+            $( "#qid_error" ).hide();
+            $.ajax({
+              url: '/inc/ajax/qitem_details.php',
+              data: { id: qitem },
+              dataType: 'json',
+              success: function(data){
+                if (typeof data.error !== 'undefined') {
+                  $( "#qid_error" ).text(data.error);
+                  $( "#qid_error" ).show();
+                }
+                $('#qid_detail_subj').text(escapeHtml(data.subject));
+                $('#qid_detail_text').text(escapeHtml(data.text_plain));
+                if (typeof data.attachments !== 'undefined') {
+                  $( "#qid_detail_atts" ).text('');
+                  $.each(data.attachments, function( index, value ) {
+                    $( "#qid_detail_atts" ).append(
+                      '<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
+                      ' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
+                    );
+                  });
+                }
+                else {
+                  $( "#qid_detail_atts" ).text('-');
+                }
+              }
+            });
+          })
+        }
+      },
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+    });
+  }
+
+  // Initial table drawings
+  draw_quarantaine_table();
+
+});

+ 450 - 353
data/web/js/u2f-api.js

@@ -1,26 +1,40 @@
-// Copyright 2014-2015 Google Inc. All rights reserved.
-//
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file or at
-// https://developers.google.com/open-source/licenses/bsd
+//Copyright 2014-2015 Google Inc. All rights reserved.
+
+//Use of this source code is governed by a BSD-style
+//license that can be found in the LICENSE file or at
+//https://developers.google.com/open-source/licenses/bsd
 
 /**
  * @fileoverview The U2F api.
  */
-
 'use strict';
 
-/** Namespace for the U2F api.
+
+/**
+ * Namespace for the U2F api.
  * @type {Object}
  */
 var u2f = u2f || {};
 
+/**
+ * FIDO U2F Javascript API Version
+ * @number
+ */
+var js_api_version;
+
 /**
  * The U2F extension id
- * @type {string}
- * @const
+ * @const {string}
  */
-u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+// The Chrome packaged app extension ID.
+// Uncomment this if you want to deploy a server instance that uses
+// the package Chrome app and does not require installing the U2F Chrome extension.
+ u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+// The U2F Chrome extension ID.
+// Uncomment this if you want to deploy a server instance that uses
+// the U2F Chrome extension to authenticate.
+// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
+
 
 /**
  * Message types for messsages to/from the extension
@@ -29,11 +43,14 @@ u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
  */
 u2f.MessageTypes = {
     'U2F_REGISTER_REQUEST': 'u2f_register_request',
-    'U2F_SIGN_REQUEST': 'u2f_sign_request',
     'U2F_REGISTER_RESPONSE': 'u2f_register_response',
-    'U2F_SIGN_RESPONSE': 'u2f_sign_response'
+    'U2F_SIGN_REQUEST': 'u2f_sign_request',
+    'U2F_SIGN_RESPONSE': 'u2f_sign_response',
+    'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
+    'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
 };
 
+
 /**
  * Response status codes
  * @const
@@ -48,17 +65,18 @@ u2f.ErrorCodes = {
     'TIMEOUT': 5
 };
 
+
 /**
- * A message type for registration requests
+ * A message for registration requests
  * @typedef {{
  *   type: u2f.MessageTypes,
- *   signRequests: Array<u2f.SignRequest>,
- *   registerRequests: ?Array<u2f.RegisterRequest>,
+ *   appId: ?string,
  *   timeoutSeconds: ?number,
  *   requestId: ?number
  * }}
  */
-u2f.Request;
+u2f.U2fRequest;
+
 
 /**
  * A message for registration responses
@@ -68,7 +86,8 @@ u2f.Request;
  *   requestId: ?number
  * }}
  */
-u2f.Response;
+u2f.U2fResponse;
+
 
 /**
  * An error object for responses
@@ -79,6 +98,19 @@ u2f.Response;
  */
 u2f.Error;
 
+/**
+ * Data object for a single sign request.
+ * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
+ */
+u2f.Transport;
+
+
+/**
+ * Data object for a single sign request.
+ * @typedef {Array<u2f.Transport>}
+ */
+u2f.Transports;
+
 /**
  * Data object for a single sign request.
  * @typedef {{
@@ -90,6 +122,7 @@ u2f.Error;
  */
 u2f.SignRequest;
 
+
 /**
  * Data object for a sign response.
  * @typedef {{
@@ -100,27 +133,51 @@ u2f.SignRequest;
  */
 u2f.SignResponse;
 
+
 /**
  * Data object for a registration request.
  * @typedef {{
  *   version: string,
- *   challenge: string,
- *   appId: string
+ *   challenge: string
  * }}
  */
 u2f.RegisterRequest;
 
+
 /**
  * Data object for a registration response.
  * @typedef {{
- *   registrationData: string,
- *   clientData: string
+ *   version: string,
+ *   keyHandle: string,
+ *   transports: Transports,
+ *   appId: string
  * }}
  */
 u2f.RegisterResponse;
 
 
-// Low level MessagePort API support
+/**
+ * Data object for a registered key.
+ * @typedef {{
+ *   version: string,
+ *   keyHandle: string,
+ *   transports: ?Transports,
+ *   appId: ?string
+ * }}
+ */
+u2f.RegisteredKey;
+
+
+/**
+ * Data object for a get API register response.
+ * @typedef {{
+ *   js_api_version: number
+ * }}
+ */
+u2f.GetJsApiVersionResponse;
+
+
+//Low level MessagePort API support
 
 /**
  * Sets up a MessagePort to the U2F extension using the
@@ -128,32 +185,34 @@ u2f.RegisterResponse;
  * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
  */
 u2f.getMessagePort = function(callback) {
-    if (typeof chrome != 'undefined' && chrome.runtime) {
-        // The actual message here does not matter, but we need to get a reply
-        // for the callback to run. Thus, send an empty signature request
-        // in order to get a failure response.
-        var msg = {
-            type: u2f.MessageTypes.U2F_SIGN_REQUEST,
-            signRequests: []
-        };
-        chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
-            if (!chrome.runtime.lastError) {
-                // We are on a whitelisted origin and can talk directly
-                // with the extension.
-                u2f.getChromeRuntimePort_(callback);
-            } else {
-                // chrome.runtime was available, but we couldn't message
-                // the extension directly, use iframe
-                u2f.getIframePort_(callback);
-            }
-        });
-    } else if (u2f.isAndroidChrome_()) {
-        u2f.getAuthenticatorPort_(callback);
-    } else {
-        // chrome.runtime was not available at all, which is normal
-        // when this origin doesn't have access to any extensions.
+  if (typeof chrome != 'undefined' && chrome.runtime) {
+    // The actual message here does not matter, but we need to get a reply
+    // for the callback to run. Thus, send an empty signature request
+    // in order to get a failure response.
+    var msg = {
+        type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+        signRequests: []
+    };
+    chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
+      if (!chrome.runtime.lastError) {
+        // We are on a whitelisted origin and can talk directly
+        // with the extension.
+        u2f.getChromeRuntimePort_(callback);
+      } else {
+        // chrome.runtime was available, but we couldn't message
+        // the extension directly, use iframe
         u2f.getIframePort_(callback);
-    }
+      }
+    });
+  } else if (u2f.isAndroidChrome_()) {
+    u2f.getAuthenticatorPort_(callback);
+  } else if (u2f.isIosChrome_()) {
+    u2f.getIosPort_(callback);
+  } else {
+    // chrome.runtime was not available at all, which is normal
+    // when this origin doesn't have access to any extensions.
+    u2f.getIframePort_(callback);
+  }
 };
 
 /**
@@ -161,22 +220,30 @@ u2f.getMessagePort = function(callback) {
  * @private
  */
 u2f.isAndroidChrome_ = function() {
-    var userAgent = navigator.userAgent;
-    return userAgent.indexOf('Chrome') != -1 &&
-        userAgent.indexOf('Android') != -1;
+  var userAgent = navigator.userAgent;
+  return userAgent.indexOf('Chrome') != -1 &&
+  userAgent.indexOf('Android') != -1;
 };
 
 /**
- * Connects directly to the extension via chrome.runtime.connect
+ * Detect chrome running on iOS based on the browser's platform.
+ * @private
+ */
+u2f.isIosChrome_ = function() {
+  return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
+};
+
+/**
+ * Connects directly to the extension via chrome.runtime.connect.
  * @param {function(u2f.WrappedChromeRuntimePort_)} callback
  * @private
  */
 u2f.getChromeRuntimePort_ = function(callback) {
-    var port = chrome.runtime.connect(u2f.EXTENSION_ID,
-        {'includeTlsChannelId': true});
-    setTimeout(function() {
-        callback(new u2f.WrappedChromeRuntimePort_(port));
-    }, 0);
+  var port = chrome.runtime.connect(u2f.EXTENSION_ID,
+      {'includeTlsChannelId': true});
+  setTimeout(function() {
+    callback(new u2f.WrappedChromeRuntimePort_(port));
+  }, 0);
 };
 
 /**
@@ -185,9 +252,20 @@ u2f.getChromeRuntimePort_ = function(callback) {
  * @private
  */
 u2f.getAuthenticatorPort_ = function(callback) {
-    setTimeout(function() {
-        callback(new u2f.WrappedAuthenticatorPort_());
-    }, 0);
+  setTimeout(function() {
+    callback(new u2f.WrappedAuthenticatorPort_());
+  }, 0);
+};
+
+/**
+ * Return a 'port' abstraction to the iOS client app.
+ * @param {function(u2f.WrappedIosPort_)} callback
+ * @private
+ */
+u2f.getIosPort_ = function(callback) {
+  setTimeout(function() {
+    callback(new u2f.WrappedIosPort_());
+  }, 0);
 };
 
 /**
@@ -197,53 +275,100 @@ u2f.getAuthenticatorPort_ = function(callback) {
  * @private
  */
 u2f.WrappedChromeRuntimePort_ = function(port) {
-    this.port_ = port;
+  this.port_ = port;
 };
 
 /**
- * Format a return a sign request.
+ * Format and return a sign request compliant with the JS API version supported by the extension.
  * @param {Array<u2f.SignRequest>} signRequests
  * @param {number} timeoutSeconds
  * @param {number} reqId
  * @return {Object}
  */
-u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
-    function(signRequests, timeoutSeconds, reqId) {
-        return {
-            type: u2f.MessageTypes.U2F_SIGN_REQUEST,
-            signRequests: signRequests,
-            timeoutSeconds: timeoutSeconds,
-            requestId: reqId
-        };
+u2f.formatSignRequest_ =
+  function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
+  if (js_api_version === undefined || js_api_version < 1.1) {
+    // Adapt request to the 1.0 JS API
+    var signRequests = [];
+    for (var i = 0; i < registeredKeys.length; i++) {
+      signRequests[i] = {
+          version: registeredKeys[i].version,
+          challenge: challenge,
+          keyHandle: registeredKeys[i].keyHandle,
+          appId: appId
+      };
+    }
+    return {
+      type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+      signRequests: signRequests,
+      timeoutSeconds: timeoutSeconds,
+      requestId: reqId
     };
+  }
+  // JS 1.1 API
+  return {
+    type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+    appId: appId,
+    challenge: challenge,
+    registeredKeys: registeredKeys,
+    timeoutSeconds: timeoutSeconds,
+    requestId: reqId
+  };
+};
 
 /**
- * Format a return a register request.
+ * Format and return a register request compliant with the JS API version supported by the extension..
  * @param {Array<u2f.SignRequest>} signRequests
  * @param {Array<u2f.RegisterRequest>} signRequests
  * @param {number} timeoutSeconds
  * @param {number} reqId
  * @return {Object}
  */
-u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
-    function(signRequests, registerRequests, timeoutSeconds, reqId) {
-        return {
-            type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
-            signRequests: signRequests,
-            registerRequests: registerRequests,
-            timeoutSeconds: timeoutSeconds,
-            requestId: reqId
-        };
+u2f.formatRegisterRequest_ =
+  function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
+  if (js_api_version === undefined || js_api_version < 1.1) {
+    // Adapt request to the 1.0 JS API
+    for (var i = 0; i < registerRequests.length; i++) {
+      registerRequests[i].appId = appId;
+    }
+    var signRequests = [];
+    for (var i = 0; i < registeredKeys.length; i++) {
+      signRequests[i] = {
+          version: registeredKeys[i].version,
+          challenge: registerRequests[0],
+          keyHandle: registeredKeys[i].keyHandle,
+          appId: appId
+      };
+    }
+    return {
+      type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+      signRequests: signRequests,
+      registerRequests: registerRequests,
+      timeoutSeconds: timeoutSeconds,
+      requestId: reqId
     };
+  }
+  // JS 1.1 API
+  return {
+    type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+    appId: appId,
+    registerRequests: registerRequests,
+    registeredKeys: registeredKeys,
+    timeoutSeconds: timeoutSeconds,
+    requestId: reqId
+  };
+};
+
 
 /**
  * Posts a message on the underlying channel.
  * @param {Object} message
  */
 u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
-    this.port_.postMessage(message);
+  this.port_.postMessage(message);
 };
 
+
 /**
  * Emulates the HTML 5 addEventListener interface. Works only for the
  * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
@@ -252,16 +377,16 @@ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
  */
 u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
     function(eventName, handler) {
-        var name = eventName.toLowerCase();
-        if (name == 'message' || name == 'onmessage') {
-            this.port_.onMessage.addListener(function(message) {
-                // Emulate a minimal MessageEvent object
-                handler({'data': message});
-            });
-        } else {
-            console.error('WrappedChromeRuntimePort only supports onMessage');
-        }
-    };
+  var name = eventName.toLowerCase();
+  if (name == 'message' || name == 'onmessage') {
+    this.port_.onMessage.addListener(function(message) {
+      // Emulate a minimal MessageEvent object
+      handler({'data': message});
+    });
+  } else {
+    console.error('WrappedChromeRuntimePort only supports onMessage');
+  }
+};
 
 /**
  * Wrap the Authenticator app with a MessagePort interface.
@@ -269,8 +394,8 @@ u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
  * @private
  */
 u2f.WrappedAuthenticatorPort_ = function() {
-    this.requestId_ = -1;
-    this.requestObject_ = null;
+  this.requestId_ = -1;
+  this.requestObject_ = null;
 }
 
 /**
@@ -278,28 +403,39 @@ u2f.WrappedAuthenticatorPort_ = function() {
  * @param {Object} message
  */
 u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
-    var intentLocation = /** @type {string} */ (message);
-    document.location = intentLocation;
+  var intentUrl =
+    u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+    ';S.request=' + encodeURIComponent(JSON.stringify(message)) +
+    ';end';
+  document.location = intentUrl;
 };
 
+/**
+ * Tells what type of port this is.
+ * @return {String} port type
+ */
+u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
+  return "WrappedAuthenticatorPort_";
+};
+
+
 /**
  * Emulates the HTML 5 addEventListener interface.
  * @param {string} eventName
  * @param {function({data: Object})} handler
  */
-u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
-    function(eventName, handler) {
-        var name = eventName.toLowerCase();
-        if (name == 'message') {
-            var self = this;
-            /* Register a callback to that executes when
-             * chrome injects the response. */
-            window.addEventListener(
-                'message', self.onRequestUpdate_.bind(self, handler), false);
-        } else {
-            console.error('WrappedAuthenticatorPort only supports message');
-        }
-    };
+u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
+  var name = eventName.toLowerCase();
+  if (name == 'message') {
+    var self = this;
+    /* Register a callback to that executes when
+     * chrome injects the response. */
+    window.addEventListener(
+        'message', self.onRequestUpdate_.bind(self, handler), false);
+  } else {
+    console.error('WrappedAuthenticatorPort only supports message');
+  }
+};
 
 /**
  * Callback invoked  when a response is received from the Authenticator.
@@ -308,72 +444,18 @@ u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
  */
 u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
     function(callback, message) {
-        var messageObject = JSON.parse(message.data);
-        var intentUrl = messageObject['intentURL'];
-
-        var errorCode = messageObject['errorCode'];
-        var responseObject = null;
-        if (messageObject.hasOwnProperty('data')) {
-            responseObject = /** @type {Object} */ (
-                JSON.parse(messageObject['data']));
-            responseObject['requestId'] = this.requestId_;
-        }
-
-        /* Sign responses from the authenticator do not conform to U2F,
-         * convert to U2F here. */
-        responseObject = this.doResponseFixups_(responseObject);
-        callback({'data': responseObject});
-    };
+  var messageObject = JSON.parse(message.data);
+  var intentUrl = messageObject['intentURL'];
 
-/**
- * Fixup the response provided by the Authenticator to conform with
- * the U2F spec.
- * @param {Object} responseData
- * @return {Object} the U2F compliant response object
- */
-u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
-    function(responseObject) {
-        if (responseObject.hasOwnProperty('responseData')) {
-            return responseObject;
-        } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
-            // Only sign responses require fixups.  If this is not a response
-            // to a sign request, then an internal error has occurred.
-            return {
-                'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
-                'responseData': {
-                    'errorCode': u2f.ErrorCodes.OTHER_ERROR,
-                    'errorMessage': 'Internal error: invalid response from Authenticator'
-                }
-            };
-        }
-
-        /* Non-conformant sign response, do fixups. */
-        var encodedChallengeObject = responseObject['challenge'];
-        if (typeof encodedChallengeObject !== 'undefined') {
-            var challengeObject = JSON.parse(atob(encodedChallengeObject));
-            var serverChallenge = challengeObject['challenge'];
-            var challengesList = this.requestObject_['signData'];
-            var requestChallengeObject = null;
-            for (var i = 0; i < challengesList.length; i++) {
-                var challengeObject = challengesList[i];
-                if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
-                    requestChallengeObject = challengeObject;
-                    break;
-                }
-            }
-        }
-        var responseData = {
-            'errorCode': responseObject['resultCode'],
-            'keyHandle': responseObject['keyHandle'],
-            'signatureData': responseObject['signature'],
-            'clientData': encodedChallengeObject
-        };
-        return {
-            'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
-            'responseData': responseData,
-            'requestId': responseObject['requestId']
-        }
-    };
+  var errorCode = messageObject['errorCode'];
+  var responseObject = null;
+  if (messageObject.hasOwnProperty('data')) {
+    responseObject = /** @type {Object} */ (
+        JSON.parse(messageObject['data']));
+  }
+
+  callback({'data': responseObject});
+};
 
 /**
  * Base URL for intents to Authenticator.
@@ -381,126 +463,44 @@ u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
  * @private
  */
 u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
-    'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
+  'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
 
 /**
- * Format a return a sign request.
- * @param {Array<u2f.SignRequest>} signRequests
- * @param {number} timeoutSeconds (ignored for now)
- * @param {number} reqId
- * @return {string}
- */
-u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
-    function(signRequests, timeoutSeconds, reqId) {
-        if (!signRequests || signRequests.length == 0) {
-            return null;
-        }
-        /* TODO(fixme): stash away requestId, as the authenticator app does
-         * not return it for sign responses. */
-        this.requestId_ = reqId;
-        /* TODO(fixme): stash away the signRequests, to deal with the legacy
-         * response format returned by the Authenticator app. */
-        this.requestObject_ = {
-            'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
-            'signData': signRequests,
-            'requestId': reqId,
-            'timeout': timeoutSeconds
-        };
-
-        var appId = signRequests[0]['appId'];
-        var intentUrl =
-            u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
-            ';S.appId=' + encodeURIComponent(appId) +
-            ';S.eventId=' + reqId +
-            ';S.challenges=' +
-            encodeURIComponent(
-                JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
-        return intentUrl;
-    };
+ * Wrap the iOS client app with a MessagePort interface.
+ * @constructor
+ * @private
+ */
+u2f.WrappedIosPort_ = function() {};
 
 /**
- * Get the browser data objects from the challenge list
- * @param {Array} challenges list of challenges
- * @return {Array} list of browser data objects
- * @private
+ * Launch the iOS client app request
+ * @param {Object} message
  */
-u2f.WrappedAuthenticatorPort_
-    .prototype.getBrowserDataList_ = function(challenges) {
-    return challenges
-        .map(function(challenge) {
-            var browserData = {
-                'typ': 'navigator.id.getAssertion',
-                'challenge': challenge['challenge']
-            };
-            var challengeObject = {
-                'challenge' : browserData,
-                'keyHandle' : challenge['keyHandle']
-            };
-            return challengeObject;
-        });
+u2f.WrappedIosPort_.prototype.postMessage = function(message) {
+  var str = JSON.stringify(message);
+  var url = "u2f://auth?" + encodeURI(str);
+  location.replace(url);
 };
 
 /**
- * Format a return a register request.
- * @param {Array<u2f.SignRequest>} signRequests
- * @param {Array<u2f.RegisterRequest>} enrollChallenges
- * @param {number} timeoutSeconds (ignored for now)
- * @param {number} reqId
- * @return {Object}
+ * Tells what type of port this is.
+ * @return {String} port type
  */
-u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
-    function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
-        if (!enrollChallenges || enrollChallenges.length == 0) {
-            return null;
-        }
-        // Assume the appId is the same for all enroll challenges.
-        var appId = enrollChallenges[0]['appId'];
-        var registerRequests = [];
-        for (var i = 0; i < enrollChallenges.length; i++) {
-            var registerRequest = {
-                'challenge': enrollChallenges[i]['challenge'],
-                'version': enrollChallenges[i]['version']
-            };
-            if (enrollChallenges[i]['appId'] != appId) {
-                // Only include the appId when it differs from the first appId.
-                registerRequest['appId'] = enrollChallenges[i]['appId'];
-            }
-            registerRequests.push(registerRequest);
-        }
-        var registeredKeys = [];
-        if (signRequests) {
-            for (i = 0; i < signRequests.length; i++) {
-                var key = {
-                    'keyHandle': signRequests[i]['keyHandle'],
-                    'version': signRequests[i]['version']
-                };
-                // Only include the appId when it differs from the appId that's
-                // being registered now.
-                if (signRequests[i]['appId'] != appId) {
-                    key['appId'] = signRequests[i]['appId'];
-                }
-                registeredKeys.push(key);
-            }
-        }
-        var request = {
-            'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
-            'appId': appId,
-            'registerRequests': registerRequests,
-            'registeredKeys': registeredKeys,
-            'requestId': reqId,
-            'timeoutSeconds': timeoutSeconds
-        };
-        var intentUrl =
-            u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
-            ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
-            ';end';
-        /* TODO(fixme): stash away requestId, this is is not necessary for
-         * register requests, but here to keep parity with sign.
-         */
-        this.requestId_ = reqId;
-        return intentUrl;
-    };
+u2f.WrappedIosPort_.prototype.getPortType = function() {
+  return "WrappedIosPort_";
+};
 
+/**
+ * Emulates the HTML 5 addEventListener interface.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
+  var name = eventName.toLowerCase();
+  if (name !== 'message') {
+    console.error('WrappedIosPort only supports message');
+  }
+};
 
 /**
  * Sets up an embedded trampoline iframe, sourced from the extension.
@@ -508,33 +508,33 @@ u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
  * @private
  */
 u2f.getIframePort_ = function(callback) {
-    // Create the iframe
-    var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
-    var iframe = document.createElement('iframe');
-    iframe.src = iframeOrigin + '/u2f-comms.html';
-    iframe.setAttribute('style', 'display:none');
-    document.body.appendChild(iframe);
-
-    var channel = new MessageChannel();
-    var ready = function(message) {
-        if (message.data == 'ready') {
-            channel.port1.removeEventListener('message', ready);
-            callback(channel.port1);
-        } else {
-            console.error('First event on iframe port was not "ready"');
-        }
-    };
-    channel.port1.addEventListener('message', ready);
-    channel.port1.start();
-
-    iframe.addEventListener('load', function() {
-        // Deliver the port to the iframe and initialize
-        iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
-    });
+  // Create the iframe
+  var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
+  var iframe = document.createElement('iframe');
+  iframe.src = iframeOrigin + '/u2f-comms.html';
+  iframe.setAttribute('style', 'display:none');
+  document.body.appendChild(iframe);
+
+  var channel = new MessageChannel();
+  var ready = function(message) {
+    if (message.data == 'ready') {
+      channel.port1.removeEventListener('message', ready);
+      callback(channel.port1);
+    } else {
+      console.error('First event on iframe port was not "ready"');
+    }
+  };
+  channel.port1.addEventListener('message', ready);
+  channel.port1.start();
+
+  iframe.addEventListener('load', function() {
+    // Deliver the port to the iframe and initialize
+    iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
+  });
 };
 
 
-// High-level JS API
+//High-level JS API
 
 /**
  * Default extension response timeout in seconds.
@@ -577,22 +577,22 @@ u2f.callbackMap_ = {};
  * @private
  */
 u2f.getPortSingleton_ = function(callback) {
-    if (u2f.port_) {
-        callback(u2f.port_);
-    } else {
-        if (u2f.waitingForPort_.length == 0) {
-            u2f.getMessagePort(function(port) {
-                u2f.port_ = port;
-                u2f.port_.addEventListener('message',
-                    /** @type {function(Event)} */ (u2f.responseHandler_));
-
-                // Careful, here be async callbacks. Maybe.
-                while (u2f.waitingForPort_.length)
-                    u2f.waitingForPort_.shift()(u2f.port_);
-            });
-        }
-        u2f.waitingForPort_.push(callback);
+  if (u2f.port_) {
+    callback(u2f.port_);
+  } else {
+    if (u2f.waitingForPort_.length == 0) {
+      u2f.getMessagePort(function(port) {
+        u2f.port_ = port;
+        u2f.port_.addEventListener('message',
+            /** @type {function(Event)} */ (u2f.responseHandler_));
+
+        // Careful, here be async callbacks. Maybe.
+        while (u2f.waitingForPort_.length)
+          u2f.waitingForPort_.shift()(u2f.port_);
+      });
     }
+    u2f.waitingForPort_.push(callback);
+  }
 };
 
 /**
@@ -601,51 +601,148 @@ u2f.getPortSingleton_ = function(callback) {
  * @private
  */
 u2f.responseHandler_ = function(message) {
-    var response = message.data;
-    var reqId = response['requestId'];
-    if (!reqId || !u2f.callbackMap_[reqId]) {
-        console.error('Unknown or missing requestId in response.');
-        return;
-    }
-    var cb = u2f.callbackMap_[reqId];
-    delete u2f.callbackMap_[reqId];
-    cb(response['responseData']);
+  var response = message.data;
+  var reqId = response['requestId'];
+  if (!reqId || !u2f.callbackMap_[reqId]) {
+    console.error('Unknown or missing requestId in response.');
+    return;
+  }
+  var cb = u2f.callbackMap_[reqId];
+  delete u2f.callbackMap_[reqId];
+  cb(response['responseData']);
 };
 
 /**
  * Dispatches an array of sign requests to available U2F tokens.
- * @param {Array<u2f.SignRequest>} signRequests
+ * If the JS API version supported by the extension is unknown, it first sends a
+ * message to the extension to find out the supported API version and then it sends
+ * the sign request.
+ * @param {string=} appId
+ * @param {string=} challenge
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
  * @param {function((u2f.Error|u2f.SignResponse))} callback
  * @param {number=} opt_timeoutSeconds
  */
-u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
-    u2f.getPortSingleton_(function(port) {
-        var reqId = ++u2f.reqCounter_;
-        u2f.callbackMap_[reqId] = callback;
-        var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
-            opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
-        var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
-        port.postMessage(req);
-    });
+u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
+  if (js_api_version === undefined) {
+    // Send a message to get the extension to JS API version, then send the actual sign request.
+    u2f.getApiVersion(
+        function (response) {
+          js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
+          console.log("Extension JS API Version: ", js_api_version);
+          u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
+        });
+  } else {
+    // We know the JS API version. Send the actual sign request in the supported API version.
+    u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
+  }
+};
+
+/**
+ * Dispatches an array of sign requests to available U2F tokens.
+ * @param {string=} appId
+ * @param {string=} challenge
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
+  u2f.getPortSingleton_(function(port) {
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+    var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
+    port.postMessage(req);
+  });
 };
 
 /**
  * Dispatches register requests to available U2F tokens. An array of sign
  * requests identifies already registered tokens.
+ * If the JS API version supported by the extension is unknown, it first sends a
+ * message to the extension to find out the supported API version and then it sends
+ * the register request.
+ * @param {string=} appId
  * @param {Array<u2f.RegisterRequest>} registerRequests
- * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
  * @param {function((u2f.Error|u2f.RegisterResponse))} callback
  * @param {number=} opt_timeoutSeconds
  */
-u2f.register = function(registerRequests, signRequests,
-                        callback, opt_timeoutSeconds) {
-    u2f.getPortSingleton_(function(port) {
-        var reqId = ++u2f.reqCounter_;
-        u2f.callbackMap_[reqId] = callback;
-        var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
-            opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
-        var req = port.formatRegisterRequest_(
-            signRequests, registerRequests, timeoutSeconds, reqId);
-        port.postMessage(req);
-    });
-};
+u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
+  if (js_api_version === undefined) {
+    // Send a message to get the extension to JS API version, then send the actual register request.
+    u2f.getApiVersion(
+        function (response) {
+          js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
+          console.log("Extension JS API Version: ", js_api_version);
+          u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
+              callback, opt_timeoutSeconds);
+        });
+  } else {
+    // We know the JS API version. Send the actual register request in the supported API version.
+    u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
+        callback, opt_timeoutSeconds);
+  }
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * @param {string=} appId
+ * @param {Array<u2f.RegisterRequest>} registerRequests
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
+  u2f.getPortSingleton_(function(port) {
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+    var req = u2f.formatRegisterRequest_(
+        appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
+    port.postMessage(req);
+  });
+};
+
+
+/**
+ * Dispatches a message to the extension to find out the supported
+ * JS API version.
+ * If the user is on a mobile phone and is thus using Google Authenticator instead
+ * of the Chrome extension, don't send the request and simply return 0.
+ * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
+ u2f.getPortSingleton_(function(port) {
+   // If we are using Android Google Authenticator or iOS client app,
+   // do not fire an intent to ask which JS API version to use.
+   if (port.getPortType) {
+     var apiVersion;
+     switch (port.getPortType()) {
+       case 'WrappedIosPort_':
+       case 'WrappedAuthenticatorPort_':
+         apiVersion = 1.1;
+         break;
+
+       default:
+         apiVersion = 0;
+         break;
+     }
+     callback({ 'js_api_version': apiVersion });
+     return;
+   }
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var req = {
+      type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
+      timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
+          opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
+      requestId: reqId
+    };
+    port.postMessage(req);
+  });
+};

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

@@ -86,14 +86,14 @@ jQuery(function($){
   function draw_sync_job_table() {
     ft_syncjob_table = FooTable.init('#sync_job_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
         {"name":"server_w_port","title":"Server"},
         {"name":"enc1","title":lang.encryption,"breakpoints":"xs sm"},
         {"name":"user1","title":lang.username},
-        {"name":"exclude","title":lang.excludes,"breakpoints":"xs sm"},
-        {"name":"mins_interval","title":lang.interval + " (min)"},
-        {"name":"last_run","title":lang.last_run,"breakpoints":"xs sm"},
+        {"name":"exclude","title":lang.excludes,"breakpoints":"all"},
+        {"name":"mins_interval","title":lang.interval + " (min)","breakpoints":"all"},
+        {"name":"last_run","title":lang.last_run,"breakpoints":"all"},
         {"name":"log","title":"Log"},
         {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active},
         {"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status},

+ 416 - 1
data/web/json_api.php

@@ -15,6 +15,41 @@ delete/alias => POST data:
 header('Content-Type: application/json');
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 error_reporting(0);
+
+function api_log($postarray) {
+  global $redis;
+  $data_var = array();
+  foreach ($postarray as $data => &$value) {
+    if ($data == 'csrf_token') {
+      continue;
+    }
+    if ($value = json_decode($value, true)) {
+      unset($value["csrf_token"]);
+      $value = json_encode($value);
+    }
+    $data_var[] = $data . "='" . $value . "'";
+  }
+  try {
+    $log_line = array(
+      'time' => time(),
+      'uri' => $_SERVER['REQUEST_URI'],
+      'method' => $_SERVER['REQUEST_METHOD'],
+      'remote' => $_SERVER['REMOTE_ADDR'],
+      'data' => implode(', ', $data_var)
+    );
+    $redis->lPush('API_LOG', json_encode($log_line));
+  }
+  catch (RedisException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'Redis: '.$e
+    );
+    return false;
+  }
+}
+
+api_log($_POST);
+
 if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
   if (isset($_GET['query'])) {
 
@@ -522,6 +557,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "bcc":
+            if (isset($_POST['attr'])) {
+              $attr = (array)json_decode($_POST['attr'], true);
+              if (bcc('add', $attr) === false) {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'error',
+                    'msg' => 'Cannot add item'
+                  ));
+                }
+              }
+              else {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'success',
+                    'msg' => 'Task completed'
+                  ));
+                }
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Cannot find attributes in post data'
+              ));
+            }
+          break;
         }
       break;
       case "get":
@@ -719,6 +787,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               break;
             }
           break;
+
           case "relayhost":
             switch ($object) {
               case "all":
@@ -837,6 +906,54 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                   echo '{}';
                 }
               break;
+              case "watchdog":
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
+                  $logs = get_logs('watchdog-mailcow', $extra);
+                }
+                else {
+                  $logs = get_logs('watchdog-mailcow');
+                }
+                if (isset($logs) && !empty($logs)) {
+                  echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+              case "acme":
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
+                  $logs = get_logs('acme-mailcow', $extra);
+                }
+                else {
+                  $logs = get_logs('acme-mailcow');
+                }
+                if (isset($logs) && !empty($logs)) {
+                  echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+              case "api":
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
+                  $logs = get_logs('api-mailcow', $extra);
+                }
+                else {
+                  $logs = get_logs('api-mailcow');
+                }
+                if (isset($logs) && !empty($logs)) {
+                  echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
               case "rspamd-history":
                 // 0 is first record, so empty is fine
                 if (isset($extra)) {
@@ -1034,6 +1151,41 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               break;
             }
           break;
+          case "bcc":
+            switch ($object) {
+              case "all":
+                $bcc_items = bcc('get');
+                if (!empty($bcc_items)) {
+                  foreach ($bcc_items as $bcc_item) {
+                    if ($details = bcc('details', $bcc_item)) {
+                      $data[] = $details;
+                    }
+                    else {
+                      continue;
+                    }
+                  }
+                }
+                if (!isset($data) || empty($data)) {
+                  echo '{}';
+                }
+                else {
+                  echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+              break;
+              default:
+                $data = bcc('details', $object);
+                if (!empty($data)) {
+                  $data[] = $details;
+                }
+                if (!isset($data) || empty($data)) {
+                  echo '{}';
+                }
+                else {
+                  echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+              break;
+            }
+          break;
           case "policy_wl_mailbox":
             switch ($object) {
               default:
@@ -1161,6 +1313,29 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               break;
             }
           break;
+          case "quarantaine":
+            // "all" will not print details
+            switch ($object) {
+              case "all":
+                $data = quarantaine('get');
+                if (!isset($data) || empty($data)) {
+                  echo '{}';
+                }
+                else {
+                  echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+              break;
+              default:
+                $data = quarantaine('details', $object);
+                if (!isset($data) || empty($data)) {
+                  echo '{}';
+                }
+                else {
+                  echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+              break;
+            }
+          break;
           case "alias-domain":
             switch ($object) {
               case "all":
@@ -1464,6 +1639,88 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "qitem":
+            if (isset($_POST['items'])) {
+              $items = (array)json_decode($_POST['items'], true);
+              if (is_array($items)) {
+                if (quarantaine('delete', array('id' => $items)) === false) {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'Deletion of items/s failed'
+                    ));
+                  }
+                }
+                else {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'Task completed'
+                    ));
+                  }
+                }
+              }
+              else {
+                echo json_encode(array(
+                  'type' => 'error',
+                  'msg' => 'Cannot find id array in post data'
+                ));
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Cannot find items in post data'
+              ));
+            }
+          break;
+          case "bcc":
+            if (isset($_POST['items'])) {
+              $items = (array)json_decode($_POST['items'], true);
+              if (is_array($items)) {
+                if (bcc('delete', array('id' => $items)) === false) {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'Deletion of items/s failed'
+                    ));
+                  }
+                }
+                else {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'Task completed'
+                    ));
+                  }
+                }
+              }
+              else {
+                echo json_encode(array(
+                  'type' => 'error',
+                  'msg' => 'Cannot find id array in post data'
+                ));
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Cannot find items in post data'
+              ));
+            }
+          break;
           case "fwdhost":
             if (isset($_POST['items'])) {
               $items = (array)json_decode($_POST['items'], true);
@@ -1919,6 +2176,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
       break;
       case "edit":
         switch ($category) {
+          case "bcc":
+            if (isset($_POST['items']) && isset($_POST['attr'])) {
+              $items = (array)json_decode($_POST['items'], true);
+              $attr = (array)json_decode($_POST['attr'], true);
+              $postarray = array_merge(array('id' => $items), $attr);
+              if (is_array($postarray['id'])) {
+                if (bcc('edit', $postarray) === false) {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'Edit failed'
+                    ));
+                  }
+                  exit();
+                }
+                else {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'Task completed'
+                    ));
+                  }
+                }
+              }
+              else {
+                echo json_encode(array(
+                  'type' => 'error',
+                  'msg' => 'Incomplete post data'
+                ));
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Incomplete post data'
+              ));
+            }
+          break;
           case "alias":
             if (isset($_POST['items']) && isset($_POST['attr'])) {
               $items = (array)json_decode($_POST['items'], true);
@@ -2137,6 +2438,85 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "qitem":
+            if (isset($_POST['items']) && isset($_POST['attr'])) {
+              $items = (array)json_decode($_POST['items'], true);
+              $attr = (array)json_decode($_POST['attr'], true);
+              $postarray = array_merge(array('id' => $items), $attr);
+              if (is_array($postarray['id'])) {
+                if (quarantaine('edit', $postarray) === false) {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'Edit failed'
+                    ));
+                  }
+                  exit();
+                }
+                else {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'Task completed'
+                    ));
+                  }
+                }
+              }
+              else {
+                echo json_encode(array(
+                  'type' => 'error',
+                  'msg' => 'Incomplete post data'
+                ));
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Incomplete post data'
+              ));
+            }
+          break;
+          case "quarantaine":
+            // Edit settings, does not need IDs
+            if (isset($_POST['attr'])) {
+              $postarray = json_decode($_POST['attr'], true);
+              if (quarantaine('edit', $postarray) === false) {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'error',
+                    'msg' => 'Edit failed'
+                  ));
+                }
+                exit();
+              }
+              else {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'success',
+                    'msg' => 'Task completed'
+                  ));
+                }
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Incomplete post data'
+              ));
+            }
+          break;
           case "time_limited_alias":
             if (isset($_POST['items']) && isset($_POST['attr'])) {
               $items = (array)json_decode($_POST['items'], true);
@@ -2364,7 +2744,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               $attr = (array)json_decode($_POST['attr'], true);
               $postarray = array_merge(array('domain' => $items), $attr);
               if (is_array($postarray['domain'])) {
-                if (mailbox('edit', 'domain', $postarray) === false) {
+                if (mailbox('edit', 'domain', $postarray)) {
                   if (isset($_SESSION['return'])) {
                     echo json_encode($_SESSION['return']);
                   }
@@ -2657,6 +3037,41 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "ui_texts":
+            // No items
+            if (isset($_POST['attr'])) {
+              $attr = (array)json_decode($_POST['attr'], true);
+              if (customize('edit', 'ui_texts', $attr) === false) {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'error',
+                    'msg' => 'Edit failed'
+                  ));
+                }
+                exit();
+              }
+              else {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'success',
+                    'msg' => 'Task completed'
+                  ));
+                }
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Incomplete post data'
+              ));
+            }
+          break;
           case "self":
             // No items, logged-in user, users and domain admins
             if ($_SESSION['mailcow_cc_role'] == "domainadmin") {

+ 92 - 5
data/web/lang/lang.de.php

@@ -5,9 +5,9 @@
 
 $lang['footer']['loading'] = 'Einen Moment bitte...';
 $lang['header']['restart_sogo'] = 'SOGo neustarten';
-$lang['footer']['restart_sogo'] = 'SOGo neustarten';
+$lang['footer']['restart_container'] = 'Container neustarten';
 $lang['footer']['restart_now'] = 'Jetzt neustarten';
-$lang['footer']['restart_sogo_info'] = 'Einige Änderungen an Domains benötigen einen Neustart SOGos. Hier können Sie SOGo neustarten.<br><br><b>Wichtig:</b> Ein korrekter Neustart SOGos kann eine Weile in Anspruch nehmen, bitte warten Sie, bis der Prozess vollständig beendet wurde.';
+$lang['footer']['restart_container_info'] = '<b>Wichtig:</b> Ein korrekter Neustart eines Containers kann eine Weile in Anspruch nehmen, bitte warten Sie, bis der Prozess vollständig beendet wurde.';
 
 $lang['footer']['confirm_delete'] = 'Löschen bestätigen';
 $lang['footer']['delete_these_items'] = 'Sind Sie sicher, dass die Änderungen an Elementen mit folgender ID durchgeführt werden sollen?';
@@ -73,7 +73,7 @@ $lang['danger']['resource_invalid'] = 'Ressourcenname ist ungültig';
 $lang['danger']['description_invalid'] = 'Ressourcenbeschreibung ist ungültig';
 $lang['danger']['mailbox_invalid_suggest'] = 'Mailboxname ist ungültig, meinten Sie vielleicht %s?';
 $lang['danger']['is_alias'] = '%s lautet bereits eine Alias-Adresse';
-$lang['danger']['is_alias_or_mailbox'] = "Eine Mailbox oder ein Alias mit der Adresse %s ist bereits vorhanden";
+$lang['danger']['is_alias_or_mailbox'] = "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden";
 $lang['danger']['is_spam_alias'] = '%s lautet bereits eine Spam-Alias-Adresse';
 $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein';
 $lang['danger']['domain_not_found'] = 'Domain %s nicht gefunden';
@@ -121,6 +121,8 @@ $lang['user']['did_you_know'] = '<b>Wussten Sie schon?</b> Sie können Ihre E-Ma
 $lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse';
 $lang['user']['alias'] = 'Alias';
 $lang['user']['aliases'] = 'Aliasse';
+$lang['user']['shared_aliases'] = 'Geteilte Alias-Adressen';
+$lang['user']['direct_aliases'] = 'Direkte Alias-Adressen';
 $lang['user']['domain_aliases'] = 'Domain-Alias Adressen';
 $lang['user']['is_catch_all'] = 'Ist Catch-All Adresse für Domain(s)';
 $lang['user']['aliases_also_send_as'] = 'Darf außerdem versenden als Benutzer';
@@ -248,7 +250,7 @@ $lang['mailbox']['target_domain'] = 'Ziel-Domain';
 $lang['mailbox']['target_address'] = 'Ziel-Adresse';
 $lang['mailbox']['username'] = 'Benutzername';
 $lang['mailbox']['fname'] = 'Name';
-$lang['mailbox']['filter_table'] = 'Tabelle filtern';
+$lang['mailbox']['filter_table'] = 'Filtern';
 $lang['mailbox']['yes'] = '&#10004;';
 $lang['mailbox']['no'] = '&#10008;';
 $lang['mailbox']['quota'] = 'Speicherplatz';
@@ -491,6 +493,8 @@ $lang['admin']['dkim_key_unused'] = 'Key ohne Zuweisung';
 $lang['admin']['dkim_key_missing'] = 'Key fehlt';
 $lang['admin']['dkim_key_hint'] = 'Der Selector für DKIM-Keys lautet immer <code>dkim</code>.';
 $lang['admin']['add'] = 'Hinzufügen';
+$lang['add']['add_domain_restart'] = 'Domain hinzufügen und SOGo neustarten';
+$lang['add']['add_domain_only'] = 'Nur Domain hinzufügen';
 $lang['admin']['configuration'] = 'Konfiguration';
 $lang['admin']['password'] = 'Passwort';
 $lang['admin']['password_repeat'] = 'Passwort (Wiederholung)';
@@ -533,6 +537,10 @@ $lang['admin']['host'] = 'Host';
 $lang['admin']['source'] = 'Quelle';
 $lang['admin']['add_forwarding_host'] = 'Weiterleitungs-Host hinzufügen';
 $lang['admin']['add_relayhost'] = 'Relayhost hinzufügen';
+$lang['admin']['api_allow_from'] = "IP-Adressen für Zugriff";
+$lang['admin']['api_key'] = "API-Key";
+$lang['admin']['activate_api'] = "API aktivieren";
+$lang['admin']['regen_api_key'] = "API-Key regenerieren";
 $lang['delete']['remove_forwardinghost_warning'] = '<b>Warnung:</b> Sie entfernen den Weiterleitungs-Host <b>%s</b>!';
 $lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt";
 $lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt";
@@ -544,6 +552,8 @@ $lang['diagnostics']['dns_records_name'] = 'Name';
 $lang['diagnostics']['dns_records_type'] = 'Typ';
 $lang['diagnostics']['dns_records_data'] = 'Korrekte Daten';
 $lang['diagnostics']['dns_records_status'] = 'Aktueller Status';
+$lang['diagnostics']['optional'] = 'Dieser Eintrag ist optional.';
+$lang['diagnostics']['cname_from_a'] = 'Wert abgeleitet von A/AAAA Eintrag. Wird unterstützt, sofern der Eintrag auf die korrekte Ressource zeigt.';
 $lang['admin']['relay_from'] = "Absenderadresse";
 $lang['admin']['relay_run'] = "Test durchführen";
 $lang['admin']['customize'] = "Anpassung";
@@ -559,4 +569,81 @@ $lang['admin']['reset_default'] = "Auf Standard zurücksetzen";
 $lang['admin']['merged_vars_hint'] = 'Ausgegraute Zeilen wurden aus der Datei <code>vars.inc.(local.)php</code> gelesen und können nicht mittels UI verändert werden.';
 $lang['mailbox']['waiting'] = "Wartend";
 $lang['mailbox']['status'] = "Status";
-$lang['mailbox']['running'] = "In Ausführung";
+$lang['mailbox']['running'] = "In Ausführung";
+
+$lang['admin']['ui_texts'] = "UI Label und Texte";
+$lang['admin']['help_text'] = "Hilfstext unter Login-Maske (HTML zulässig)";
+$lang['admin']['main_name'] = '"mailcow UI" Name';
+$lang['admin']['apps_name'] = '"mailcow Apps" Name';
+
+$lang['admin']['customize'] = "UI Anpassung";
+$lang['admin']['change_logo'] = "Logo ändern";
+$lang['admin']['logo_info'] = "Die hochgeladene Grafik wird für die Navigationsleiste auf eine Höhe von 40px skaliert. Für die Darstellung auf der Login-Maske beträgt die skalierte Breite maximal 250px. Eine frei skalierbare Grafik (etwa SVG) wird empfohlen.";
+$lang['admin']['upload'] = "Hochladen";
+$lang['admin']['app_links'] = "App Links";
+$lang['admin']['app_name'] = "App Name";
+$lang['admin']['link'] = "Link";
+$lang['admin']['remove_row'] = "Entfernen";
+$lang['admin']['add_row'] = "Reihe hinzufügen";
+$lang['admin']['reset_default'] = "Zurücksetzen auf Standard";
+$lang['admin']['merged_vars_hint'] = 'Ausgegraute Reihen wurden aus der Datei <code>vars.inc.(local.)php</code> gelesen und können hier nicht verändert werden.';
+
+$lang['edit']['tls_policy'] = "TLS Policy ändern";
+$lang['edit']['spam_score'] = "Einen benutzerdefiniterten Spam-Score festlegen";
+$lang['edit']['spam_policy'] = "Hinzufügen und Entfernen von Einträgen in White- und Blacklists";
+$lang['edit']['delimiter_action'] = "Delimiter Aktion verändern";
+$lang['edit']['syncjobs'] = "Sync job hinzufügen oder anpassen";
+$lang['edit']['eas_reset'] = "ActiveSync Geräte-Cache zurücksetzen";
+$lang['edit']['spam_alias'] = "Anpassen temporärer Alias-Adressen";
+
+$lang['danger']['img_tmp_missing'] = "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen";
+$lang['danger']['img_invalid'] = "Grafik konnte nicht validiert werden";
+$lang['danger']['invalid_mime_type'] = "Grafik konnte nicht validiert werden: Ungültiger MIME-Type";
+$lang['success']['upload_success'] = "Datei wurde erfolgreich hochgeladen";
+$lang['success']['app_links'] = "Änderungen an App Links wurden gespeichert";
+$lang['success']['ui_texts'] = "Änderungen an UI-Texten";
+$lang['success']['reset_main_logo'] = "Standardgrafik wurde wiederhergestellt";
+$lang['success']['items_released'] = "Ausgewählte Objekte wurden an Mailbox versendet";
+$lang['danger']['imagick_exception'] = "Fataler Bildverarbeitungsfehler";
+
+$lang['quarantaine']['quarantaine'] = "Quarantäne";
+$lang['quarantaine']['qinfo'] = "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank. Die Nachricht wird <em>nicht</em> angekommen. Der Absender erhält keinen Eindruck einer zugestellten E-Mail.<br />
+  E-Mails mit einer maximalen Größe von 10 MiB werden gespeichert.";
+$lang['quarantaine']['release'] = "Freigeben";
+$lang['quarantaine']['empty'] = 'Keine Einträge';
+$lang['quarantaine']['toggle_all'] = 'Alle auswählen';
+$lang['quarantaine']['quick_actions'] = 'Aktionen';
+$lang['quarantaine']['remove'] = 'Entfernen';
+$lang['quarantaine']['received'] = "Empfangen";
+$lang['quarantaine']['action'] = "Aktion";
+$lang['quarantaine']['rcpt'] = "Empfänger";
+$lang['quarantaine']['qid'] = "Rspamd QID";
+$lang['quarantaine']['sender'] = "Sender";
+$lang['quarantaine']['show_item'] = "Details";
+$lang['quarantaine']['check_hash'] = "Checksumme auf VirusTotal suchen";
+$lang['quarantaine']['qitem'] = "Quarantäneeintrag";
+$lang['quarantaine']['subj'] = "Betreff";
+$lang['quarantaine']['text_plain_content'] = "Inhalt (text/plain)";
+$lang['quarantaine']['atts'] = "Anhänge";
+
+$lang['header']['quarantaine'] = "Quarantäne";
+$lang['header']['debug'] = "Debugging";
+
+$lang['quarantaine']['release_body'] = "Die ursprüngliche Nachricht wurde als EML-Datei im Anhang hinterlegt.";
+$lang['danger']['release_send_failed'] = "Die Nachricht konnte nicht versendet werden: %s";
+$lang['quarantaine']['release_subject'] = "Potentiell schädliche Nachricht aus Quarantäne: %s";
+
+$lang['mailbox']['bcc_map_type'] = "BCC Typ";
+$lang['mailbox']['bcc_type'] = "BCC Typ";
+$lang['mailbox']['bcc_sender_map'] = "Senderabhängig";
+$lang['mailbox']['bcc_rcpt_map'] = "Empfängerabhängig";
+$lang['mailbox']['bcc_local_dest'] = "Lokales Ziel";
+$lang['mailbox']['bcc_destinations'] = "BCC Ziel(e)";
+
+$lang['mailbox']['bcc'] = "BCC";
+$lang['mailbox']['bcc_maps'] = "BCC-Maps";
+$lang['mailbox']['bcc_to_sender'] = "Map senderabhängig verwenden";
+$lang['mailbox']['bcc_to_rcpt'] = "Map empfängerabhängig verwenden";
+$lang['mailbox']['add_bcc_entry'] = "BCC-Eintrag hinzufügen";
+$lang['mailbox']['bcc_info'] = "Eine empfängerabhängige Map wird verwendet, wenn die BCC-Map Eintragung auf den Eingang einer E-Mail auf das lokale Ziel reagieren soll. Senderabhängige Maps verfahren nach dem gleichen Prinzip.<br/>
+  Das lokale Ziel wird bei Fehlzustellungen an ein BCC-Ziel nicht informiert.";

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

@@ -5,9 +5,9 @@
 
 $lang['footer']['loading'] = "Please wait...";
 $lang['header']['restart_sogo'] = 'Restart SOGo';
-$lang['footer']['restart_sogo'] = 'Restart SOGo';
+$lang['footer']['restart_container'] = 'Restart container';
 $lang['footer']['restart_now'] = 'Restart now';
-$lang['footer']['restart_sogo_info'] = 'Some tasks, e.g. adding a domain, require you to restart SOGo to catch changes made in the mailcow UI.<br><br><b>Important:</b> A graceful restart may take a while to complete, please wait for it to finish.';
+$lang['footer']['restart_container_info'] = '<b>Important:</b> A graceful restart may take a while to complete, please wait for it to finish.';
 
 $lang['footer']['confirm_delete'] = 'Confirm deletion';
 $lang['footer']['delete_these_items'] = 'Please confirm your changes to the following object id:';
@@ -73,7 +73,7 @@ $lang['danger']['description_invalid'] = 'Resource description is invalid';
 $lang['danger']['resource_invalid'] = "Resource name is invalid";
 $lang['danger']['mailbox_invalid_suggest'] = 'Mailbox name is invalid, did you mean to type "%s"?';
 $lang['danger']['is_alias'] = "%s is already known as an alias address";
-$lang['danger']['is_alias_or_mailbox'] = "%s is already known as an alias or a mailbox";
+$lang['danger']['is_alias_or_mailbox'] = "%s is already known as an alias, a mailbox or an alias address expanded from an alias domain.";
 $lang['danger']['is_spam_alias'] = "%s is already known as a spam alias address";
 $lang['danger']['quota_not_0_not_numeric'] = "Quota must be numeric and >= 0";
 $lang['danger']['domain_not_found'] = 'Domain %s not found';
@@ -121,6 +121,8 @@ $lang['user']['did_you_know'] = '<b>Did you know?</b> You can use tags in your e
 $lang['user']['spam_aliases'] = 'Temporary email aliases';
 $lang['user']['alias'] = 'Alias';
 $lang['user']['aliases'] = 'Aliases';
+$lang['user']['shared_aliases'] = 'Shared alias addresses';
+$lang['user']['direct_aliases'] = 'Direct alias addresses';
 $lang['user']['domain_aliases'] = 'Domain alias addresses';
 $lang['user']['is_catch_all'] = 'Catch-all for domain/s';
 $lang['user']['aliases_also_send_as'] = 'Also allowed to send as user';
@@ -272,6 +274,7 @@ $lang['mailbox']['deactivate'] = 'Deactivate';
 $lang['mailbox']['owner'] = 'Owner';
 $lang['mailbox']['mins_interval'] = 'Interval (min)';
 $lang['mailbox']['last_run'] = 'Last run';
+$lang['mailbox']['excludes'] = 'Excludes';
 $lang['mailbox']['last_run_reset'] = 'Schedule next';
 $lang['mailbox']['sieve_info'] = 'You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.<br>
 Each filter will be processed in the described order. Neither a failed script nor an issued "keep;" will stop processing of further scripts.<br>
@@ -496,6 +499,8 @@ $lang['admin']['sys_info'] = 'System information';
 $lang['admin']['dkim_add_key'] = 'Add ARC/DKIM key';
 $lang['admin']['dkim_keys'] = 'ARC/DKIM keys';
 $lang['admin']['add'] = 'Add';
+$lang['add']['add_domain_restart'] = 'Add domain and restart SOGo';
+$lang['add']['add_domain_only'] = 'Add domain only';
 $lang['admin']['configuration'] = 'Configuration';
 $lang['admin']['password'] = 'Password';
 $lang['admin']['password_repeat'] = 'Confirmation password (repeat)';
@@ -549,8 +554,19 @@ $lang['diagnostics']['dns_records_name'] = 'Name';
 $lang['diagnostics']['dns_records_type'] = 'Type';
 $lang['diagnostics']['dns_records_data'] = 'Correct Data';
 $lang['diagnostics']['dns_records_status'] = 'Current State';
+$lang['diagnostics']['optional'] = 'This record is optional.';
+$lang['diagnostics']['cname_from_a'] = 'Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.';
+
 $lang['admin']['relay_from'] = '"From:" address';
-$lang['admin']['relay_run'] = "Run test";
+$lang['admin']['api_allow_from'] = "Allow API access from these IPs";
+$lang['admin']['api_key'] = "API key";
+$lang['admin']['activate_api'] = "Activate API";
+$lang['admin']['regen_api_key'] = "Regenerate API key";
+
+$lang['admin']['ui_texts'] = "UI labels and texts";
+$lang['admin']['help_text'] = "Override help text below login mask (HTML allowed)";
+$lang['admin']['main_name'] = '"mailcow UI" name';
+$lang['admin']['apps_name'] = '"mailcow Apps" name';
 
 $lang['admin']['customize'] = "Customize";
 $lang['admin']['change_logo'] = "Change logo";
@@ -574,3 +590,55 @@ $lang['edit']['delimiter_action'] = "Change delimiter action";
 $lang['edit']['syncjobs'] = "Add or change sync jobs";
 $lang['edit']['eas_reset'] = "Reset EAS devices";
 $lang['edit']['spam_alias'] = "Create or change time limited alias addresses";
+
+$lang['danger']['img_tmp_missing'] = "Cannot validate image file: Temporary file not found";
+$lang['danger']['img_invalid'] = "Cannot validate image file";
+$lang['danger']['invalid_mime_type'] = "Invalid mime type";
+$lang['success']['upload_success'] = "File uploaded successfully";
+$lang['success']['app_links'] = "Saved changes to app links";
+$lang['success']['ui_texts'] = "Saved changes to UI texts";
+$lang['success']['reset_main_logo'] = "Reset to default logo";
+$lang['success']['items_released'] = "Selected items were released";
+$lang['danger']['imagick_exception'] = "Error: Imagick exception while reading image";
+
+$lang['quarantaine']['quarantaine'] = "Quarantaine";
+$lang['quarantaine']['qinfo'] = "The quarantaine system will save rejected mail to the database, while the sender will <em>not</em> be given the impression of a delivered mail.<br />
+  Only mails up to 10 MiB will be saved in the quarantaine.";
+$lang['quarantaine']['release'] = "Release";
+$lang['quarantaine']['empty'] = 'No results';
+$lang['quarantaine']['toggle_all'] = 'Toggle all';
+$lang['quarantaine']['quick_actions'] = 'Actions';
+$lang['quarantaine']['remove'] = 'Remove';
+$lang['quarantaine']['received'] = "Received";
+$lang['quarantaine']['action'] = "Action";
+$lang['quarantaine']['rcpt'] = "Recipient";
+$lang['quarantaine']['qid'] = "Rspamd QID";
+$lang['quarantaine']['sender'] = "Sender";
+$lang['quarantaine']['show_item'] = "Show item";
+$lang['quarantaine']['check_hash'] = "Search file hash @ VT";
+$lang['quarantaine']['qitem'] = "Quarantaine item";
+$lang['quarantaine']['subj'] = "Subject";
+$lang['quarantaine']['text_plain_content'] = "Content (text/plain)";
+$lang['quarantaine']['atts'] = "Attachments";
+
+$lang['header']['quarantaine'] = "Quarantaine";
+$lang['header']['debug'] = "Debug";
+
+$lang['quarantaine']['release_body'] = "We have attached your message as eml file to this message.";
+$lang['danger']['release_send_failed'] = "Message could not be released: %s";
+$lang['quarantaine']['release_subject'] = "Potentially damaging quarantaine item %s";
+
+$lang['mailbox']['bcc_map_type'] = "BCC type";
+$lang['mailbox']['bcc_type'] = "BCC type";
+$lang['mailbox']['bcc_sender_map'] = "Sender map";
+$lang['mailbox']['bcc_rcpt_map'] = "Recipient map";
+$lang['mailbox']['bcc_local_dest'] = "Local destination";
+$lang['mailbox']['bcc_destinations'] = "BCC destination/s";
+
+$lang['mailbox']['bcc'] = "BCC";
+$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']['bcc_info'] = "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.";

+ 28 - 0
data/web/mailbox.php

@@ -21,6 +21,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
     </li>
     <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']['bcc_maps'];?></a></li>
   </ul>
 
 	<div class="row">
@@ -207,6 +208,33 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
           </div>
         </div>
 
+        <div role="tabpanel" class="tab-pane" id="tab-bcc">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h3 class="panel-title"><?=$lang['mailbox']['bcc_maps'];?></h3>
+            </div>
+            <p style="margin:10px" class="help-block"><?=$lang['mailbox']['bcc_info'];?></p>
+            <div class="table-responsive">
+              <table class="table table-striped" id="bcc_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="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 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 role="separator" class="divider"></li>
+                  <li><a id="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>
       </div> <!-- /tab-content -->
     </div> <!-- /col-md-12 -->
   </div> <!-- /row -->

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

@@ -13,7 +13,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['admin']['add_domain_admin'];?></h3>
       </div>
       <div class="modal-body">
-          <form class="form-horizontal" data-id="domain_admin" role="form" method="post">
+          <form class="form-horizontal" data-cached-form="true" data-id="domain_admin" role="form" method="post">
             <div class="form-group">
               <label class="control-label col-sm-2" for="username"><?=$lang['admin']['username'];?>:</label>
               <div class="col-sm-10">
@@ -71,7 +71,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><span class="glyphicon glyphicon-stats"></span> Relayhost</h3>
       </div>
       <div class="modal-body">
-          <form class="form-horizontal" id="test_relayhost_form" role="form" method="post">
+          <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">
             <div class="form-group">
               <label class="control-label col-sm-2" for="mail_from"><?=$lang['admin']['relay_from'];?></label>

+ 6 - 0
data/web/modals/debug.php

@@ -0,0 +1,6 @@
+<?php
+if (!isset($_SESSION['mailcow_cc_role'])) {
+	header('Location: /');
+	exit();
+}
+?>

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

@@ -49,7 +49,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
             <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
           </div>
           <hr>
-          <p><?=$lang['tfa']['waiting_usb_register'];?></p>
+          <p id="u2f_status_reg"></p>
           <div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
           <input type="hidden" name="token" id="u2f_register_data"/>
           <input type="hidden" name="tfa_method" value="u2f">
@@ -146,7 +146,7 @@ if (isset($_SESSION['pending_tfa_method'])):
         case "u2f":
       ?>
         <form role="form" method="post" id="u2f_auth_form">
-          <p><?=$lang['tfa']['waiting_usb_auth'];?></p>
+          <p id="u2f_status_auth"></p>
           <div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
           <input type="hidden" name="token" id="u2f_auth_data"/>
           <input type="hidden" name="tfa_method" value="u2f">
@@ -183,19 +183,19 @@ if (isset($_SESSION['pending_tfa_method'])):
 endif;
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin'):
 ?>
-<div id="RestartSOGo" class="modal fade" role="dialog">
+<div id="RestartContainer" class="modal fade" role="dialog">
   <div class="modal-dialog">
     <div class="modal-content">
     <div class="modal-header">
       <button type="button" class="close" data-dismiss="modal">&times;</button>
-      <h4 class="modal-title"><?= $lang['footer']['restart_sogo']; ?></h4>
+      <h4 class="modal-title"><?= $lang['footer']['restart_container']; ?> (<code id="containerName"></code>)</h4>
     </div>
     <div class="modal-body">
-      <p><?= $lang['footer']['restart_sogo_info']; ?></p>
+      <p><?= $lang['footer']['restart_container_info']; ?></p>
       <hr>
-      <button class="btn btn-md btn-primary" id="triggerRestartSogo"><?= $lang['footer']['restart_now']; ?></button>
+      <button class="btn btn-md btn-primary" id="triggerRestartContainer"><?= $lang['footer']['restart_now']; ?></button>
       <br><br>
-      <div id="statusTriggerRestartSogo"></div>
+      <div id="statusTriggerRestartContainer"></div>
     </div>
     </div>
   </div>

+ 93 - 16
data/web/modals/mailbox.php

@@ -13,7 +13,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['mailbox']['add_mailbox'];?></h3>
       </div>
       <div class="modal-body">
-        <form class="form-horizontal" data-id="add_mailbox" role="form">
+        <form class="form-horizontal" data-cached-form="true" data-id="add_mailbox" role="form">
           <div class="form-group">
             <label class="control-label col-sm-2" for="local_part"><?=$lang['add']['mailbox_username'];?></label>
             <div class="col-sm-10">
@@ -86,7 +86,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['mailbox']['add_domain'];?></h3>
       </div>
       <div class="modal-body">
-				<form class="form-horizontal" data-id="add_domain" role="form">
+				<form class="form-horizontal" data-cached-form="true" data-id="add_domain" role="form">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
 						<div class="col-sm-10">
@@ -96,7 +96,7 @@ 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">
+						<input type="text" class="form-control" name="description" id="description" required>
 						</div>
 					</div>
 					<div class="form-group">
@@ -143,7 +143,8 @@ 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" data-api-url='add/domain' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+              <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>
 						</div>
 					</div>
 					<p><span class="glyphicon glyphicon-exclamation-sign text-danger"></span> <?=$lang['add']['restart_sogo_hint'];?></p>
@@ -161,7 +162,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['mailbox']['add_resource'];?></h3>
       </div>
       <div class="modal-body">
-				<form class="form-horizontal" role="form" data-id="add_resource">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_resource">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
 						<div class="col-sm-10">
@@ -223,7 +224,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['mailbox']['add_alias'];?></h3>
       </div>
       <div class="modal-body">
-				<form class="form-horizontal" role="form" data-id="add_alias">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias">
 					<input type="hidden" value="0" name="active">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="address"><?=$lang['add']['alias_address'];?></label>
@@ -268,7 +269,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['mailbox']['add_domain_alias'];?></h3>
       </div>
       <div class="modal-body">
-				<form class="form-horizontal" role="form" data-id="add_alias_domain">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias_domain">
 					<input type="hidden" value="0" name="active">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="alias_domain"><?=$lang['add']['alias_domain'];?></label>
@@ -316,7 +317,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
       </div>
       <div class="modal-body">
         <p class="help-block"><?=$lang['add']['syncjob_hint'];?></p>
-				<form class="form-horizontal" role="form" data-id="add_syncjob">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob">
           <div class="form-group">
             <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
             <div class="col-sm-10">
@@ -417,13 +418,6 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 							</div>
 						</div>
 					</div>
-					<div class="form-group">
-						<div class="col-sm-offset-2 col-sm-10">
-							<div class="checkbox">
-							<label><input type="checkbox" value="1" name="delete2"> <?=$lang['add']['delete2'];?></label>
-							</div>
-						</div>
-					</div>
 					<div class="form-group">
 						<div class="col-sm-offset-2 col-sm-10">
 							<div class="checkbox">
@@ -450,7 +444,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title">Filter</h3>
       </div>
       <div class="modal-body">
-				<form class="form-horizontal" role="form" data-id="add_filter">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_filter">
           <div class="form-group">
             <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
             <div class="col-sm-10">
@@ -509,6 +503,77 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
   </div>
 </div><!-- add add_filter modal -->
+<!-- add add_bcc modal -->
+<div class="modal fade" id="addBCCModalAdmin" 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']['bcc_maps'];?></h3>
+      </div>
+      <div class="modal-body">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_bcc">
+          <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 id="addSelectLocalDest" name="local_dest" id="local_dest" required>
+              <?php
+              $domains = mailbox('get', 'domains');
+              $alias_domains = mailbox('get', 'alias_domains');
+              if (!empty($domains)) {
+                foreach ($domains as $domain) {
+                  echo "<option>".htmlspecialchars($domain)."</option>";
+                }
+              }
+              if (!empty($alias_domains)) {
+                foreach ($alias_domains as $alias_domain) {
+                  echo "<option>".htmlspecialchars($alias_domain)."</option>";
+                }
+              }
+              if (!empty($domains)) {
+                foreach ($domains as $domain) {
+                  $mailboxes = mailbox('get', 'mailboxes', $domain);
+                  foreach ($mailboxes as $mailbox) {
+                    echo "<option>".htmlspecialchars($mailbox)."</option>";
+                  }
+                }
+              }
+              ?>
+              </select>
+            </div>
+          </div>
+          <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>
+                <option value="sender"><?=$lang['mailbox']['bcc_sender_map'];?></option>
+                <option value="rcpt"><?=$lang['mailbox']['bcc_rcpt_map'];?></option>
+              </select>
+            </div>
+          </div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="bcc_dest"><?=$lang['mailbox']['bcc_destinations'];?>:</label>
+						<div class="col-sm-10">
+							<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control" rows="20" id="bcc_dest" name="bcc_dest" required></textarea>
+						</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" id="add_item" data-id="add_bcc" data-api-url='add/bcc' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button>
+						</div>
+					</div>
+				</form>
+      </div>
+    </div>
+  </div>
+</div><!-- add add_bcc modal -->
 <!-- log modal -->
 <div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
   <div class="modal-dialog modal-lg" role="document">
@@ -520,3 +585,15 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
   </div>
 </div><!-- log modal -->
+<!-- DNS info modal -->
+<div class="modal fade" id="dnsInfoModal" tabindex="-1" role="dialog" aria-labelledby="dnsInfoModalLabel">
+  <div class="modal-dialog modal-lg" role="document">
+    <div class="modal-content">
+      <div class="modal-header"><h4 class="modal-title"><?=$lang['diagnostics']['dns_records'];?></h4></div>
+      <div class="modal-body">
+        <p><?=$lang['diagnostics']['dns_records_24hours'];?></p>
+        <div class="dns-modal-body"></div>
+      </div>
+    </div>
+  </div>
+</div><!-- DNS info modal -->

+ 32 - 0
data/web/modals/quarantaine.php

@@ -0,0 +1,32 @@
+<?php
+if (!isset($_SESSION['mailcow_cc_role'])) {
+	header('Location: /');
+	exit();
+}
+?>
+<div class="modal fade" id="qidDetailModal" 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"><span class="glyphicon glyphicon-info"></span> <?=$lang['quarantaine']['qitem'];?></h3>
+      </div>
+      <div class="modal-body">
+        <div id="qid_error" style="display:none" class="alert alert-danger"></div>
+        <div class="form-group">
+          <label for="qid_detail_subj"><h4><?=$lang['quarantaine']['subj'];?>:</h4></label>
+          <p id="qid_detail_subj"></p>
+        </div>
+        <div class="form-group">
+          <label for="qid_detail_text"><h4><?=$lang['quarantaine']['text_plain_content'];?>:</h4></label>
+          <pre id="qid_detail_text"></pre>
+        </div>
+        <div class="form-group">
+          <label for="qid_detail_atts"><h4><?=$lang['quarantaine']['atts'];?>:</h4></label>
+          <div id="qid_detail_atts">-</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+

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

@@ -14,7 +14,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
       </div>
       <div class="modal-body">
         <p><?=$lang['add']['syncjob_hint'];?></p>
-				<form class="form-horizontal" role="form" data-id="add_syncjob">
+				<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob">
 					<div class="form-group">
 						<label class="control-label col-sm-2" for="host1"><?=$lang['add']['hostname'];?></label>
 						<div class="col-sm-10">
@@ -130,7 +130,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
   <div class="modal-dialog" role="document">
     <div class="modal-content">
       <div class="modal-body">
-        <form class="form-horizontal" data-id="pwchange" role="form" method="post" autocomplete="off">
+        <form class="form-horizontal" data-cached-form="true" data-id="pwchange" role="form" method="post" autocomplete="off">
           <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">

+ 56 - 0
data/web/quarantaine.php

@@ -0,0 +1,56 @@
+<?php
+require_once "inc/prerequisites.inc.php";
+
+if (isset($_SESSION['mailcow_cc_role'])) {
+require_once "inc/header.inc.php";
+$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+
+?>
+<div class="container">
+	<div class="row">
+		<div class="col-md-12">
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <h3 class="panel-title"><?=$lang['quarantaine']['quarantaine'];?></h3>
+        </div>
+        <p style="margin:10px" class="help-block"><?=$lang['quarantaine']['qinfo'];?></p>
+        <div class="table-responsive">
+          <table id="quarantainetable" class="table table-striped"></table>
+        </div>
+        <div class="mass-actions-quarantaine">
+          <div class="btn-group">
+            <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['quarantaine']['toggle_all'];?></a>
+            <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['quarantaine']['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['quarantaine']['release'];?></a></li>
+              <li role="separator" class="divider"></li>
+              <li><a id="delete_selected" data-id="qitems" data-api-url='delete/qitem' href="#"><?=$lang['quarantaine']['remove'];?></a></li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div> <!-- /col-md-12 -->
+  </div> <!-- /row -->
+</div> <!-- /container -->
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/quarantaine.php';
+?>
+<script type='text/javascript'>
+<?php
+$lang_mailbox = json_encode($lang['quarantaine']);
+echo "var lang = ". $lang_mailbox . ";\n";
+echo "var csrf_token = '". $_SESSION['CSRF']['TOKEN'] . "';\n";
+$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
+echo "var role = '". $role . "';\n";
+echo "var pagination_size = '". $PAGINATION_SIZE . "';\n";
+?>
+</script>
+<script src="js/footable.min.js"></script>
+<script src="js/quarantaine.js"></script>
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
+} else {
+	header('Location: /');
+	exit();
+}
+?>

+ 12 - 5
data/web/user.php

@@ -23,7 +23,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
     </div>
     <hr>
     <div class="row">
-      <div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?></div>
+      <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?></div>
         <div class="col-sm-9 col-xs-7">
           <p id="tfa_pretty"><?=$tfa_data['pretty'];?></p>
             <div id="tfa_additional">
@@ -40,8 +40,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
         </div>
     </div>
     <div class="row">
-      <div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?></div>
-      <div class="col-md-9 col-xs-7">
+      <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?></div>
+      <div class="col-sm-9 col-xs-7">
         <select id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
           <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
           <option value="u2f"><?=$lang['tfa']['u2f'];?></option>
@@ -105,11 +105,18 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   $user_get_alias_details = user_get_alias_details($username);
   ?>
   <div class="row">
-    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['aliases'];?>:</div>
+    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['direct_aliases'];?>:</div>
     <div class="col-md-9 col-xs-7">
-    <p><?=$user_get_alias_details['aliases'];?></p>
+    <p><?=$user_get_alias_details['direct_aliases'];?></p>
     </div>
   </div>
+  <div class="row">
+    <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['shared_aliases'];?>:</div>
+    <div class="col-md-9 col-xs-7">
+    <p><?=$user_get_alias_details['shared_aliases'];?></p>
+    </div>
+  </div>
+  <hr>
   <div class="row">
     <div class="col-md-3 col-xs-5 text-right"><?=$lang['user']['domain_aliases'];?>:</div>
     <div class="col-md-9 col-xs-7">

+ 36 - 12
docker-compose.yml

@@ -2,9 +2,11 @@ version: '2.1'
 services:
 
     unbound-mailcow:
-      image: mailcow/unbound:1.0
+      image: mailcow/unbound:1.1
       build: ./data/Dockerfiles/unbound
       command: /usr/sbin/unbound
+      environment:
+        - TZ=${TZ}
       volumes:
         - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
       restart: always
@@ -16,11 +18,11 @@ services:
 
     mysql-mailcow:
       image: mariadb:10.2
-      command: mysqld --max_allowed_packet=192M --max-connections=1500 --innodb-strict-mode=0 --skip-host-cache --skip-name-resolve --log-warnings=0
       volumes:
         - mysql-vol-1:/var/lib/mysql/
         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
       environment:
+        - TZ=${TZ}
         - MYSQL_ROOT_PASSWORD=${DBROOT}
         - MYSQL_DATABASE=${DBNAME}
         - MYSQL_USER=${DBUSER}
@@ -39,6 +41,8 @@ services:
       volumes:
         - redis-vol-1:/data/
       restart: always
+      environment:
+        - TZ=${TZ}
       dns:
         - 172.22.1.254
       networks:
@@ -48,11 +52,12 @@ services:
             - redis
 
     clamd-mailcow:
-      image: mailcow/clamd:1.5
+      image: mailcow/clamd:1.6
       build: ./data/Dockerfiles/clamd
       restart: always
       environment:
         - SKIP_CLAMD=${SKIP_CLAMD:-n}
+        - TZ=${TZ}
       dns:
         - 172.22.1.254
       networks:
@@ -61,11 +66,13 @@ services:
             - clamd
 
     rspamd-mailcow:
-      image: mailcow/rspamd:1.14
+      image: mailcow/rspamd:1.15
       build: ./data/Dockerfiles/rspamd
       stop_grace_period: 30s
       depends_on:
         - nginx-mailcow
+      environment:
+        - TZ=${TZ}
       volumes:
         - ./data/conf/rspamd/custom/:/etc/rspamd/custom:ro
         - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:ro
@@ -84,7 +91,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.5
+      image: mailcow/phpfpm:1.7
       build: ./data/Dockerfiles/phpfpm
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
@@ -93,7 +100,10 @@ services:
         - ./data/web:/web:rw
         - ./data/conf/rspamd/dynmaps:/dynmaps:ro
         - dkim-vol-1:/data/dkim
+        - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro
       environment:
+        - LOG_LINES=${LOG_LINES}
+        - TZ=${TZ}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
@@ -115,13 +125,14 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:1.10
+      image: mailcow/sogo:1.13
       build: ./data/Dockerfiles/sogo
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
         - TZ=${TZ}
+        - LOG_LINES=${LOG_LINES}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
       volumes:
         - ./data/conf/sogo/:/etc/sogo/
@@ -135,7 +146,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.12
+      image: mailcow/dovecot:1.14
       build: ./data/Dockerfiles/dovecot
       cap_add:
         - NET_BIND_SERVICE
@@ -146,9 +157,11 @@ services:
         - vmail-vol-1:/var/vmail
         - crypt-vol-1:/mail_crypt/
       environment:
+        - LOG_LINES=${LOG_LINES}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
+        - TZ=${TZ}
       ports:
         - "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
         - "${IMAP_PORT:-143}:143"
@@ -171,7 +184,7 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: mailcow/postfix:1.7
+      image: mailcow/postfix:1.11
       build: ./data/Dockerfiles/postfix
       volumes:
         - ./data/conf/postfix:/opt/postfix/conf
@@ -179,6 +192,8 @@ services:
         - postfix-vol-1:/var/spool/postfix
         - crypt-vol-1:/var/lib/zeyple
       environment:
+        - LOG_LINES=${LOG_LINES}
+        - TZ=${TZ}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
@@ -228,6 +243,7 @@ services:
         - ./data/conf/rspamd/dynmaps:/dynmaps:ro
         - ./data/assets/ssl/:/etc/ssl/mail/:ro
         - ./data/conf/nginx/:/etc/nginx/conf.d/:rw
+        - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro
       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}"
@@ -244,11 +260,12 @@ services:
       depends_on:
         - nginx-mailcow
         - mysql-mailcow
-      image: mailcow/acme:1.24
+      image: mailcow/acme:1.27
       build: ./data/Dockerfiles/acme
       dns:
         - 172.22.1.254
       environment:
+        - LOG_LINES=${LOG_LINES}
         - ADDITIONAL_SAN=${ADDITIONAL_SAN}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
         - DBNAME=${DBNAME}
@@ -267,7 +284,7 @@ services:
             - acme
 
     fail2ban-mailcow:
-      image: mailcow/fail2ban:1.9
+      image: mailcow/fail2ban:1.10
       build: ./data/Dockerfiles/fail2ban
       stop_grace_period: 30s
       depends_on:
@@ -288,12 +305,16 @@ services:
         - /lib/modules:/lib/modules:ro
 
     watchdog-mailcow:
-      image: mailcow/watchdog:1.10
+      image: mailcow/watchdog:1.12
+      # Debug
+      #command: /watchdog.sh
       build: ./data/Dockerfiles/watchdog
       volumes:
         - vmail-vol-1:/vmail:ro
       restart: always
       environment:
+        - LOG_LINES=${LOG_LINES}
+        - TZ=${TZ}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
@@ -307,12 +328,15 @@ services:
             - watchdog
 
     dockerapi-mailcow:
-      image: mailcow/dockerapi:1.2
+      image: mailcow/dockerapi:1.4
       restart: always
       build: ./data/Dockerfiles/dockerapi
       oom_score_adj: -10
+      environment:
+        - TZ=${TZ}
       volumes:
         - /var/run/docker.sock:/var/run/docker.sock:ro
+        - ./data/conf/rspamd/override.d/worker-controller-password.inc:/access.inc:rw
       networks:
         mailcow-network:
           aliases:

+ 2 - 0
generate_config.sh

@@ -101,6 +101,8 @@ USE_WATCHDOG=n
 # Send notifications by mail (no DKIM signature, sent from watchdog@MAILCOW_HOSTNAME)
 #WATCHDOG_NOTIFY_EMAIL=
 
+LOG_LINES=9999
+
 EOF
 
 mkdir -p data/assets/ssl

Some files were not shown because too many files changed in this diff