Browse Source

Merge branch 'admin-login' of https://github.com/mailcow/mailcow-dockerized into admin-login

andryyy 6 years ago
parent
commit
9e96267a73
40 changed files with 673 additions and 609 deletions
  1. 4 1
      data/Dockerfiles/acme/docker-entrypoint.sh
  2. 2 2
      data/Dockerfiles/dovecot/Dockerfile
  3. 6 2
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  4. 19 16
      data/Dockerfiles/phpfpm/docker-entrypoint.sh
  5. 2 2
      data/Dockerfiles/postfix/postfix.sh
  6. 0 2
      data/Dockerfiles/rspamd/Dockerfile
  7. 5 2
      data/Dockerfiles/solr/Dockerfile
  8. 19 378
      data/Dockerfiles/solr/docker-entrypoint.sh
  9. 289 0
      data/Dockerfiles/solr/solr-config-7.7.0.xml
  10. 49 0
      data/Dockerfiles/solr/solr-schema-7.7.0.xml
  11. 21 17
      data/Dockerfiles/watchdog/watchdog.sh
  12. 6 6
      data/conf/dovecot/dovecot.conf
  13. 1 0
      data/conf/postfix/local_transport
  14. 9 5
      data/conf/postfix/main.cf
  15. 3 0
      data/conf/postfix/master.cf
  16. 30 32
      data/conf/rspamd/dynmaps/settings.php
  17. 0 16
      data/conf/rspamd/local.d/rspamd.conf.local
  18. 3 0
      data/conf/rspamd/meta_exporter/pipe.php
  19. 12 0
      data/conf/rspamd/override.d/worker-fuzzy.inc
  20. 1 1
      data/conf/rspamd/override.d/worker-proxy.inc
  21. 1 0
      data/web/admin.php
  22. 5 4
      data/web/css/build/001-bootstrap.min.css
  23. 13 0
      data/web/inc/ajax/qr_gen.php
  24. 3 0
      data/web/inc/ajax/transport_check.php
  25. 9 0
      data/web/inc/footer.inc.php
  26. 2 2
      data/web/inc/functions.inc.php
  27. 9 9
      data/web/inc/functions.mailbox.inc.php
  28. 2 1
      data/web/inc/prerequisites.inc.php
  29. 1 1
      data/web/inc/vars.inc.php
  30. 31 3
      data/web/js/site/mailbox.js
  31. 42 42
      data/web/js/site/quarantine.js
  32. 5 5
      data/web/lang/lang.de.php
  33. 5 3
      data/web/lang/lang.en.php
  34. 1 1
      data/web/modals/footer.php
  35. 0 21
      data/web/modals/mailbox.php
  36. 26 18
      data/web/sogo-auth.php
  37. 8 7
      docker-compose.yml
  38. 6 0
      generate_config.sh
  39. 3 5
      helper-scripts/nextcloud.sh
  40. 20 5
      update.sh

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

@@ -42,7 +42,6 @@ mkdir -p ${ACME_BASE}/acme
 [[ -f ${ACME_BASE}/acme/private/privkey.pem ]] && mv ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/key.pem
 [[ -f ${ACME_BASE}/acme/private/privkey.pem ]] && mv ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/key.pem
 [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/account.pem
 [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/account.pem
 
 
-
 reload_configurations(){
 reload_configurations(){
   # Reading container IDs
   # Reading container IDs
   # Wrapping as array to ensure trimmed content when calling $NGINX etc.
   # Wrapping as array to ensure trimmed content when calling $NGINX etc.
@@ -156,6 +155,7 @@ else
     exec env TRIGGER_RESTART=1 $(readlink -f "$0")
     exec env TRIGGER_RESTART=1 $(readlink -f "$0")
   fi
   fi
 fi
 fi
+chmod 600 ${ACME_BASE}/key.pem
 
 
 log_f "Waiting for database... " no_nl
 log_f "Waiting for database... " no_nl
 while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
 while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
@@ -196,6 +196,9 @@ while true; do
     log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
     log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
   fi
   fi
 
 
+  chmod 600 ${ACME_BASE}/acme/key.pem
+  chmod 600 ${ACME_BASE}/acme/account.pem
+
   # Skipping IP check when we like to live dangerously
   # Skipping IP check when we like to live dangerously
   if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
   if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
     SKIP_IP_CHECK=y
     SKIP_IP_CHECK=y

+ 2 - 2
data/Dockerfiles/dovecot/Dockerfile

@@ -3,8 +3,8 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
 ARG DEBIAN_FRONTEND=noninteractive
 ARG DEBIAN_FRONTEND=noninteractive
 ENV LC_ALL C
 ENV LC_ALL C
-ENV DOVECOT_VERSION 2.3.4
-ENV PIGEONHOLE_VERSION 0.5.4
+ENV DOVECOT_VERSION 2.3.5
+ENV PIGEONHOLE_VERSION 0.5.5
 
 
 RUN apt-get update && apt-get -y --no-install-recommends install \
 RUN apt-get update && apt-get -y --no-install-recommends install \
   automake \
   automake \

+ 6 - 2
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -106,7 +106,7 @@ chmod 644 /usr/local/etc/dovecot/mail_plugins /usr/local/etc/dovecot/mail_plugin
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf
 cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf
 driver = mysql
 driver = mysql
 connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
-user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/:VOLATILEDIR=/var/volatile/%u') AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1'
+user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u') AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1'
 iterate_query = SELECT username FROM mailbox WHERE active='1';
 iterate_query = SELECT username FROM mailbox WHERE active='1';
 EOF
 EOF
 
 
@@ -127,6 +127,10 @@ if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/v
 if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi
 if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi
 if [[ $(stat -c %U /var/attachments) != "vmail" ]] ; then chown -R vmail:vmail /var/attachments ; fi
 if [[ $(stat -c %U /var/attachments) != "vmail" ]] ; then chown -R vmail:vmail /var/attachments ; fi
 
 
+# Cleanup random user maildirs
+rm -rf /var/vmail/mailcow.local/*
+
+
 # Create random master for SOGo sieve features
 # Create random master for SOGo sieve features
 RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
 RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
 RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
 RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
@@ -189,7 +193,7 @@ echo '30 3 * * *   vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/d
 echo '* * * * *    vmail /usr/local/bin/trim_logs.sh >> /dev/console 2>&1' > /etc/cron.d/trim_logs
 echo '* * * * *    vmail /usr/local/bin/trim_logs.sh >> /dev/console 2>&1' > /etc/cron.d/trim_logs
 echo '25 * * * *   vmail /usr/local/bin/maildir_gc.sh >> /dev/console 2>&1' > /etc/cron.d/maildir_gc
 echo '25 * * * *   vmail /usr/local/bin/maildir_gc.sh >> /dev/console 2>&1' > /etc/cron.d/maildir_gc
 echo '30 1 * * *   root  /usr/local/bin/sa-rules.sh  >> /dev/console 2>&1' > /etc/cron.d/sa-rules
 echo '30 1 * * *   root  /usr/local/bin/sa-rules.sh  >> /dev/console 2>&1' > /etc/cron.d/sa-rules
-echo '0 2 * * *    root  /usr/bin/curl http://solr:8983/solr/dovecot/update?optimize=true >> /dev/console 2>&1' > /etc/cron.d/solr-optimize
+echo '0 2 * * *    root  /usr/bin/curl http://solr:8983/solr/dovecot-fts/update?optimize=true >> /dev/console 2>&1' > /etc/cron.d/solr-optimize
 echo '*/20 * * * * vmail /usr/local/bin/quarantine_notify.py >> /dev/console 2>&1' > /etc/cron.d/quarantine_notify
 echo '*/20 * * * * vmail /usr/local/bin/quarantine_notify.py >> /dev/console 2>&1' > /etc/cron.d/quarantine_notify
 
 
 # Fix more than 1 hardlink issue
 # Fix more than 1 hardlink issue

+ 19 - 16
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -25,23 +25,26 @@ CONTAINER_ID=
 # Todo: Better check if upgrade failed
 # Todo: Better check if upgrade failed
 # This can happen due to a broken sogo_view
 # This can happen due to a broken sogo_view
 [ -s /mysql_upgrade_loop ] && SQL_LOOP_C=$(cat /mysql_upgrade_loop)
 [ -s /mysql_upgrade_loop ] && SQL_LOOP_C=$(cat /mysql_upgrade_loop)
-CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | .id")
-if [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ [^a-zA-Z0-9] ]]; then
-  SQL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json' | jq -r .type)
-  if [[ ${SQL_UPGRADE_RETURN} == 'warning' ]]; then
-    if [ -z ${SQL_LOOP_C} ]; then
-      echo 1 > /mysql_upgrade_loop
-      echo "MySQL applied an upgrade, restarting PHP-FPM..."
-      exit 1
-    else
-      rm /mysql_upgrade_loop
-      echo "MySQL was not applied previously, skipping. Restart php-fpm-mailcow to retry or run mysql_upgrade manually."
-      while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
-        echo "Waiting for SQL to return..."
-        sleep 2
-      done
-    fi
+until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
+  CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | .id" 2> /dev/null)
+done
+echo "MySQL @ ${CONTAINER_ID}"
+SQL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json' | jq -r .type)
+if [[ ${SQL_UPGRADE_RETURN} == 'warning' ]]; then
+  if [ -z ${SQL_LOOP_C} ]; then
+    echo 1 > /mysql_upgrade_loop
+    echo "MySQL applied an upgrade, restarting PHP-FPM..."
+    exit 1
+  else
+    rm /mysql_upgrade_loop
+    echo "MySQL was not applied previously, skipping. Restart php-fpm-mailcow to retry or run mysql_upgrade manually."
+    while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
+      echo "Waiting for SQL to return..."
+      sleep 2
+    done
   fi
   fi
+else
+  echo "MySQL is up-to-date"
 fi
 fi
 
 
 # Trigger db init
 # Trigger db init

+ 2 - 2
data/Dockerfiles/postfix/postfix.sh

@@ -104,8 +104,8 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
   WHERE id IN (
   WHERE id IN (
     SELECT relayhost FROM domain
     SELECT relayhost FROM domain
       WHERE CONCAT('@', domain) = '%s'
       WHERE CONCAT('@', domain) = '%s'
-      OR '%s' IN (
-        SELECT CONCAT('@', alias_domain) FROM alias_domain
+      OR domain IN (
+        SELECT target_domain FROM alias_domain WHERE CONCAT('@', alias_domain) =  '%s'
       )
       )
   )
   )
   AND active = '1'
   AND active = '1'

+ 0 - 2
data/Dockerfiles/rspamd/Dockerfile

@@ -13,7 +13,6 @@ RUN apt-get update && apt-get install -y \
 	&& echo "deb https://rspamd.com/apt-stable/ bionic main" > /etc/apt/sources.list.d/rspamd.list \
 	&& echo "deb https://rspamd.com/apt-stable/ bionic main" > /etc/apt/sources.list.d/rspamd.list \
 	&& apt-get update && apt-get install -y rspamd \
 	&& apt-get update && apt-get install -y rspamd \
 	&& rm -rf /var/lib/apt/lists/* \
 	&& rm -rf /var/lib/apt/lists/* \
-	&& echo '.include $LOCAL_CONFDIR/local.d/rspamd.conf.local' > /etc/rspamd/rspamd.conf.local \
 	&& apt-get autoremove --purge \
 	&& apt-get autoremove --purge \
 	&& apt-get clean \
 	&& apt-get clean \
 	&& mkdir -p /run/rspamd \
 	&& mkdir -p /run/rspamd \
@@ -21,7 +20,6 @@ RUN apt-get update && apt-get install -y \
 
 
 COPY settings.conf /etc/rspamd/settings.conf
 COPY settings.conf /etc/rspamd/settings.conf
 COPY docker-entrypoint.sh /docker-entrypoint.sh
 COPY docker-entrypoint.sh /docker-entrypoint.sh
-COPY metadata_exporter.lua /usr/share/rspamd/lua/metadata_exporter.lua
 
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
 ENTRYPOINT ["/docker-entrypoint.sh"]
 
 

+ 5 - 2
data/Dockerfiles/solr/Dockerfile

@@ -1,9 +1,12 @@
-FROM solr:7-alpine
+FROM solr:7.7-alpine
 USER root
 USER root
 COPY docker-entrypoint.sh /
 COPY docker-entrypoint.sh /
+COPY solr-config-7.7.0.xml /
+COPY solr-schema-7.7.0.xml /
+
 
 
 RUN apk --no-cache add su-exec curl tzdata \
 RUN apk --no-cache add su-exec curl tzdata \
   && chmod +x /docker-entrypoint.sh \
   && chmod +x /docker-entrypoint.sh \
-  && /docker-entrypoint.sh --bootstrap
+  && bash /docker-entrypoint.sh --bootstrap
 
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
 ENTRYPOINT ["/docker-entrypoint.sh"]

+ 19 - 378
data/Dockerfiles/solr/docker-entrypoint.sh

@@ -18,403 +18,44 @@ fi
 
 
 set -e
 set -e
 
 
-# allow easier debugging with `docker run -e VERBOSE=yes`
-if [[ "$VERBOSE" = "yes" ]]; then
-  set -x
-fi
-
 # run the optional initdb
 # run the optional initdb
 . /opt/docker-solr/scripts/run-initdb
 . /opt/docker-solr/scripts/run-initdb
 
 
-function solr_config() {
-  curl -XPOST http://localhost:8983/solr/dovecot/schema -H 'Content-type:application/json' -d '{
-    "add-field-type":{
-      "name":"long",
-      "class":"solr.TrieLongField"
-    },
-    "add-field-type":{
-      "name":"dovecot_text",
-      "class":"solr.TextField",
-      "autoGeneratePhraseQueries":true,
-      "positionIncrementGap":100,
-      "indexAnalyser":{
-        "charFilter":{
-          "class":"solr.MappingCharFilterFactory",
-          "mapping":"mapping-FoldToASCII.txt"
-        },
-        "charFilter":{
-          "class":"solr.MappingCharFilterFactory",
-          "mapping":"mapping-ISOLatin1Accent.txt"
-        },
-        "charFilter":{
-          "class":"solr.HTMLStripCharFilterFactory"
-        },
-        "tokenizer":{
-          "class":"solr.StandardTokenizerFactory"
-        },
-        "filter":{
-          "class":"solr.StopFilterFactory",
-          "words":"stopwords.txt",
-          "ignoreCase":true
-        },
-        "filter":{
-          "class":"solr.WordDelimiterGraphFilterFactory",
-          "generateWordParts":1,
-          "generateNumberParts":1,
-          "splitOnCaseChange":1,
-          "splitOnNumerics":1,
-          "catenateWords":1,
-          "catenateNumbers":1,
-          "catenateAll":1
-        },
-        "filter":{
-          "class":"solr.FlattenGraphFilterFactory"
-        },
-        "filter":{
-          "class":"solr.LowerCaseFilterFactory"
-        },
-        "filter":{
-          "class":"solr.KeywordMarkerFilterFactory",
-          "protected":"protwords.txt"
-        },
-        "filter":{
-          "class":"solr.PorterStemFilterFactory"
-        }
-      },
-      "queryAnalyzer":{
-        "tokenizer":{
-          "class":"solr.StandardTokenizerFactory"
-        },
-        "filter":{
-          "class":"solr.SynonymGraphFilterFactory",
-          "expand":true,
-          "ignoreCase":true,
-          "synonyms":synonyms.txt
-        },
-        "filter":{
-          "class":"solr.FlattenGraphFilterFactory"
-        },
-        "filter":{
-          "class":"solr.StopFilterFactory",
-          "words":"stopwords.txt",
-          "ignoreCase":true
-        },
-        "filter":{
-          "class":"solr.WordDelimiterGraphFilterFactory",
-          "generateWordParts":1,
-          "generateNumberParts":1,
-          "splitOnCaseChange":1,
-          "splitOnNumerics":1,
-          "catenateWords":1,
-          "catenateNumbers":1,
-          "catenateAll":1
-        },
-        "filter":{
-          "class":"solr.LowerCaseFilterFactory"
-        },
-        "filter":{
-          "class":"solr.KeywordMarkerFilterFactory",
-          "protected":"protwords.txt"
-        },
-        "filter":{
-          "class":"solr.PorterStemFilterFactory"
-        }
-      }
-    },
-    "add-field":{
-      "name":"uid",
-      "type":"long",
-      "indexed":true,
-      "stored":true,
-      "required":true
-    },
-    "add-field":{
-      "name":"box",
-      "type":"string",
-      "indexed":true,
-      "stored":true,
-      "required":true
-    },
-    "add-field":{
-      "name":"user",
-      "type":"string",
-      "indexed":true,
-      "stored":true,
-      "required":true
-    },
-    "add-field":{
-      "name":"hdr",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-
-    },
-    "add-field":{
-      "name":"body",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "add-field":{
-      "name":"from",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "add-field":{
-      "name":"to",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "add-field":{
-      "name":"cc",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "add-field":{
-      "name":"bcc",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "add-field":{
-      "name":"subject",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    }
-  }'
-
-  curl -XPOST http://localhost:8983/solr/dovecot/schema -H 'Content-type:application/json' -d '{
-    "replace-field-type":{
-      "name":"long",
-      "class":"solr.TrieLongField"
-    },
-    "replace-field-type":{
-      "name":"dovecot_text",
-      "class":"solr.TextField",
-      "autoGeneratePhraseQueries":true,
-      "positionIncrementGap":100,
-      "indexAnalyser":{
-        "charFilter":{
-          "class":"solr.MappingCharFilterFactory",
-          "mapping":"mapping-FoldToASCII.txt"
-        },
-        "charFilter":{
-          "class":"solr.MappingCharFilterFactory",
-          "mapping":"mapping-ISOLatin1Accent.txt"
-        },
-        "charFilter":{
-          "class":"solr.HTMLStripCharFilterFactory"
-        },
-        "tokenizer":{
-          "class":"solr.StandardTokenizerFactory"
-        },
-        "filter":{
-          "class":"solr.StopFilterFactory",
-          "words":"stopwords.txt",
-          "ignoreCase":true
-        },
-        "filter":{
-          "class":"solr.WordDelimiterGraphFilterFactory",
-          "generateWordParts":1,
-          "generateNumberParts":1,
-          "splitOnCaseChange":1,
-          "splitOnNumerics":1,
-          "catenateWords":1,
-          "catenateNumbers":1,
-          "catenateAll":1
-        },
-        "filter":{
-          "class":"solr.FlattenGraphFilterFactory"
-        },
-        "filter":{
-          "class":"solr.LowerCaseFilterFactory"
-        },
-        "filter":{
-          "class":"solr.KeywordMarkerFilterFactory",
-          "protected":"protwords.txt"
-        },
-        "filter":{
-          "class":"solr.PorterStemFilterFactory"
-        }
-      },
-      "queryAnalyzer":{
-        "tokenizer":{
-          "class":"solr.StandardTokenizerFactory"
-        },
-        "filter":{
-          "class":"solr.SynonymGraphFilterFactory",
-          "expand":true,
-          "ignoreCase":true,
-          "synonyms":synonyms.txt
-        },
-        "filter":{
-          "class":"solr.FlattenGraphFilterFactory"
-        },
-        "filter":{
-          "class":"solr.StopFilterFactory",
-          "words":"stopwords.txt",
-          "ignoreCase":true
-        },
-        "filter":{
-          "class":"solr.WordDelimiterGraphFilterFactory",
-          "generateWordParts":1,
-          "generateNumberParts":1,
-          "splitOnCaseChange":1,
-          "splitOnNumerics":1,
-          "catenateWords":1,
-          "catenateNumbers":1,
-          "catenateAll":1
-        },
-        "filter":{
-          "class":"solr.LowerCaseFilterFactory"
-        },
-        "filter":{
-          "class":"solr.KeywordMarkerFilterFactory",
-          "protected":"protwords.txt"
-        },
-        "filter":{
-          "class":"solr.PorterStemFilterFactory"
-        }
-      }
-    },
-    "replace-field":{
-      "name":"uid",
-      "type":"long",
-      "indexed":true,
-      "stored":true,
-      "required":true
-    },
-    "replace-field":{
-      "name":"box",
-      "type":"string",
-      "indexed":true,
-      "stored":true,
-      "required":true
-    },
-    "replace-field":{
-      "name":"user",
-      "type":"string",
-      "indexed":true,
-      "stored":true,
-      "required":true
-    },
-    "replace-field":{
-      "name":"hdr",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-
-    },
-    "replace-field":{
-      "name":"body",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "replace-field":{
-      "name":"from",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "replace-field":{
-      "name":"to",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "replace-field":{
-      "name":"cc",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "replace-field":{
-      "name":"bcc",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    },
-    "replace-field":{
-      "name":"subject",
-      "type":"dovecot_text",
-      "indexed":true,
-      "stored":false
-    }
-  }'
-
-  curl -XPOST http://localhost:8983/solr/dovecot/config -H 'Content-type:application/json' -d '{
-    "update-requesthandler":{
-      "name":"/select",
-      "class":"solr.SearchHandler",
-      "defaults":{
-        "wt":"xml"
-      }
-    }
-  }'
-
-  curl -XPOST http://localhost:8983/solr/dovecot/config/updateHandler -d '{
-    "set-property": {
-      "updateHandler.autoSoftCommit.maxDocs":500,
-      "updateHandler.autoSoftCommit.maxTime":120000,
-      "updateHandler.autoCommit.maxDocs":200,
-      "updateHandler.autoCommit.maxTime":1800000,
-      "updateHandler.autoCommit.openSearcher":false
-    }
-  }'
-}
-
 # fixing volume permission
 # fixing volume permission
-
-[[ -d /opt/solr/server/solr/dovecot/data ]] && chown -R solr:solr /opt/solr/server/solr/dovecot/data
+[[ -d /opt/solr/server/solr/dovecot-fts/data ]] && chown -R solr:solr /opt/solr/server/solr/dovecot-fts/data
 if [[ "${1}" != "--bootstrap" ]]; then
 if [[ "${1}" != "--bootstrap" ]]; then
   sed -i '/SOLR_HEAP=/c\SOLR_HEAP="'${SOLR_HEAP:-1024}'m"' /opt/solr/bin/solr.in.sh
   sed -i '/SOLR_HEAP=/c\SOLR_HEAP="'${SOLR_HEAP:-1024}'m"' /opt/solr/bin/solr.in.sh
 else
 else
   sed -i '/SOLR_HEAP=/c\SOLR_HEAP="256m"' /opt/solr/bin/solr.in.sh
   sed -i '/SOLR_HEAP=/c\SOLR_HEAP="256m"' /opt/solr/bin/solr.in.sh
 fi
 fi
 
 
-# start a Solr so we can use the Schema API, but only on localhost,
-# so that clients don't see Solr until we have configured it.
-
-echo "Starting local Solr instance to setup configuration"
-su-exec solr start-local-solr
-
-# keep a sentinel file so we don't try to create the core a second time
-# for example when we restart a container.
+if [[ "${1}" == "--bootstrap" ]]; then
+  echo "Creating initial configuration"
+  echo "Modifying default config set"
+  cp /solr-config-7.7.0.xml /opt/solr/server/solr/configsets/_default/conf/solrconfig.xml
+  cp /solr-schema-7.7.0.xml /opt/solr/server/solr/configsets/_default/conf/schema.xml
+  rm /opt/solr/server/solr/configsets/_default/conf/managed-schema
 
 
-SENTINEL=/opt/docker-solr/core_created
+  echo "Starting local Solr instance to setup configuration"
+  su-exec solr start-local-solr
 
 
-if [[ -f ${SENTINEL} ]]; then
-  echo "skipping core creation"
-else
-  echo "Creating core \"dovecot\""
-  su-exec solr /opt/solr/bin/solr create -c "dovecot"
+  echo "Creating core \"dovecot-fts\""
+  su-exec solr /opt/solr/bin/solr create -c "dovecot-fts"
 
 
   # See https://github.com/docker-solr/docker-solr/issues/27
   # See https://github.com/docker-solr/docker-solr/issues/27
   echo "Checking core"
   echo "Checking core"
   while ! wget -O - 'http://localhost:8983/solr/admin/cores?action=STATUS' | grep -q instanceDir; do
   while ! wget -O - 'http://localhost:8983/solr/admin/cores?action=STATUS' | grep -q instanceDir; do
     echo "Could not find any cores, waiting..."
     echo "Could not find any cores, waiting..."
-    sleep 5
+    sleep 3
   done
   done
-  echo "Created core \"dovecot\""
-  touch ${SENTINEL}
-fi
 
 
-echo "Starting configuration"
-while ! wget -O - 'http://localhost:8983/solr/admin/cores?action=STATUS' | grep -q instanceDir; do
-  echo "Waiting for Solr..."
-  sleep 5
-done
-solr_config
-echo "Stopping local Solr"
-su-exec solr stop-local-solr
+  echo "Created core \"dovecot-fts\""
+
+  echo "Stopping local Solr"
+  su-exec solr stop-local-solr
 
 
-if [[ "${1}" == "--bootstrap" ]]; then
   exit 0
   exit 0
-else
-  exec su-exec solr solr-foreground
 fi
 fi
+
+exec su-exec solr solr-foreground
+

+ 289 - 0
data/Dockerfiles/solr/solr-config-7.7.0.xml

@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!-- This is the default config with stuff non-essential to Dovecot removed. -->
+
+<config>
+  <!-- Controls what version of Lucene various components of Solr
+       adhere to.  Generally, you want to use the latest version to
+       get all bug fixes and improvements. It is highly recommended
+       that you fully re-index after changing this setting as it can
+       affect both how text is indexed and queried.
+  -->
+  <luceneMatchVersion>7.7.0</luceneMatchVersion>
+
+  <!-- A 'dir' option by itself adds any files found in the directory
+       to the classpath, this is useful for including all jars in a
+       directory.
+
+       When a 'regex' is specified in addition to a 'dir', only the
+       files in that directory which completely match the regex
+       (anchored on both ends) will be included.
+
+       If a 'dir' option (with or without a regex) is used and nothing
+       is found that matches, a warning will be logged.
+
+       The examples below can be used to load some solr-contribs along
+       with their external dependencies.
+    -->
+  <lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" />
+  <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-cell-\d.*\.jar" />
+
+  <lib dir="${solr.install.dir:../../../..}/contrib/clustering/lib/" regex=".*\.jar" />
+  <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-clustering-\d.*\.jar" />
+
+  <lib dir="${solr.install.dir:../../../..}/contrib/langid/lib/" regex=".*\.jar" />
+  <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-langid-\d.*\.jar" />
+
+  <lib dir="${solr.install.dir:../../../..}/contrib/velocity/lib" regex=".*\.jar" />
+  <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-velocity-\d.*\.jar" />
+
+  <!-- Data Directory
+
+       Used to specify an alternate directory to hold all index data
+       other than the default ./data under the Solr home.  If
+       replication is in use, this should match the replication
+       configuration.
+    -->
+  <dataDir>${solr.data.dir:}</dataDir>
+
+  <!-- The default high-performance update handler -->
+  <updateHandler class="solr.DirectUpdateHandler2">
+
+    <!-- Enables a transaction log, used for real-time get, durability, and
+         and solr cloud replica recovery.  The log can grow as big as
+         uncommitted changes to the index, so use of a hard autoCommit
+         is recommended (see below).
+         "dir" - the target directory for transaction logs, defaults to the
+                solr data directory.
+         "numVersionBuckets" - sets the number of buckets used to keep
+                track of max version values when checking for re-ordered
+                updates; increase this value to reduce the cost of
+                synchronizing access to version buckets during high-volume
+                indexing, this requires 8 bytes (long) * numVersionBuckets
+                of heap space per Solr core.
+    -->
+    <updateLog>
+      <str name="dir">${solr.ulog.dir:}</str>
+      <int name="numVersionBuckets">${solr.ulog.numVersionBuckets:65536}</int>
+    </updateLog>
+
+    <!-- AutoCommit
+
+         Perform a hard commit automatically under certain conditions.
+         Instead of enabling autoCommit, consider using "commitWithin"
+         when adding documents.
+
+         http://wiki.apache.org/solr/UpdateXmlMessages
+
+         maxDocs - Maximum number of documents to add since the last
+                   commit before automatically triggering a new commit.
+
+         maxTime - Maximum amount of time in ms that is allowed to pass
+                   since a document was added before automatically
+                   triggering a new commit.
+         openSearcher - if false, the commit causes recent index changes
+           to be flushed to stable storage, but does not cause a new
+           searcher to be opened to make those changes visible.
+
+         If the updateLog is enabled, then it's highly recommended to
+         have some sort of hard autoCommit to limit the log size.
+      -->
+    <autoCommit>
+      <maxTime>${solr.autoCommit.maxTime:15000}</maxTime>
+      <openSearcher>false</openSearcher>
+    </autoCommit>
+
+    <!-- softAutoCommit is like autoCommit except it causes a
+         'soft' commit which only ensures that changes are visible
+         but does not ensure that data is synced to disk.  This is
+         faster and more near-realtime friendly than a hard commit.
+      -->
+    <autoSoftCommit>
+      <maxTime>${solr.autoSoftCommit.maxTime:-1}</maxTime>
+    </autoSoftCommit>
+
+    <!-- Update Related Event Listeners
+
+         Various IndexWriter related events can trigger Listeners to
+         take actions.
+
+         postCommit - fired after every commit or optimize command
+         postOptimize - fired after every optimize command
+      -->
+
+  </updateHandler>
+
+  <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+       Query section - these settings control query time things like caches
+       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
+  <query>
+    <!-- Solr Internal Query Caches
+
+         There are two implementations of cache available for Solr,
+         LRUCache, based on a synchronized LinkedHashMap, and
+         FastLRUCache, based on a ConcurrentHashMap.
+
+         FastLRUCache has faster gets and slower puts in single
+         threaded operation and thus is generally faster than LRUCache
+         when the hit ratio of the cache is high (> 75%), and may be
+         faster under other scenarios on multi-cpu systems.
+    -->
+
+    <!-- Filter Cache
+
+         Cache used by SolrIndexSearcher for filters (DocSets),
+         unordered sets of *all* documents that match a query.  When a
+         new searcher is opened, its caches may be prepopulated or
+         "autowarmed" using data from caches in the old searcher.
+         autowarmCount is the number of items to prepopulate.  For
+         LRUCache, the autowarmed items will be the most recently
+         accessed items.
+
+         Parameters:
+           class - the SolrCache implementation LRUCache or
+               (LRUCache or FastLRUCache)
+           size - the maximum number of entries in the cache
+           initialSize - the initial capacity (number of entries) of
+               the cache.  (see java.util.HashMap)
+           autowarmCount - the number of entries to prepopulate from
+               and old cache.
+           maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed
+                      to occupy. Note that when this option is specified, the size
+                      and initialSize parameters are ignored.
+      -->
+    <filterCache class="solr.FastLRUCache"
+                 size="512"
+                 initialSize="512"
+                 autowarmCount="0"/>
+
+    <!-- Query Result Cache
+
+         Caches results of searches - ordered lists of document ids
+         (DocList) based on a query, a sort, and the range of documents requested.
+         Additional supported parameter by LRUCache:
+            maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed
+                       to occupy
+      -->
+    <queryResultCache class="solr.LRUCache"
+                      size="512"
+                      initialSize="512"
+                      autowarmCount="0"/>
+
+    <!-- Document Cache
+
+         Caches Lucene Document objects (the stored fields for each
+         document).  Since Lucene internal document ids are transient,
+         this cache will not be autowarmed.
+      -->
+    <documentCache class="solr.LRUCache"
+                   size="512"
+                   initialSize="512"
+                   autowarmCount="0"/>
+
+    <!-- custom cache currently used by block join -->
+    <cache name="perSegFilter"
+           class="solr.search.LRUCache"
+           size="10"
+           initialSize="0"
+           autowarmCount="10"
+           regenerator="solr.NoOpRegenerator" />
+
+    <!-- Lazy Field Loading
+
+         If true, stored fields that are not requested will be loaded
+         lazily.  This can result in a significant speed improvement
+         if the usual case is to not load all stored fields,
+         especially if the skipped fields are large compressed text
+         fields.
+    -->
+    <enableLazyFieldLoading>true</enableLazyFieldLoading>
+
+    <!-- Result Window Size
+
+         An optimization for use with the queryResultCache.  When a search
+         is requested, a superset of the requested number of document ids
+         are collected.  For example, if a search for a particular query
+         requests matching documents 10 through 19, and queryWindowSize is 50,
+         then documents 0 through 49 will be collected and cached.  Any further
+         requests in that range can be satisfied via the cache.
+      -->
+    <queryResultWindowSize>20</queryResultWindowSize>
+
+    <!-- Maximum number of documents to cache for any entry in the
+         queryResultCache.
+      -->
+    <queryResultMaxDocsCached>200</queryResultMaxDocsCached>
+
+    <!-- Use Cold Searcher
+
+         If a search request comes in and there is no current
+         registered searcher, then immediately register the still
+         warming searcher and use it.  If "false" then all requests
+         will block until the first searcher is done warming.
+      -->
+    <useColdSearcher>false</useColdSearcher>
+
+  </query>
+
+
+  <!-- Request Dispatcher
+
+       This section contains instructions for how the SolrDispatchFilter
+       should behave when processing requests for this SolrCore.
+
+    -->
+  <requestDispatcher>
+    <httpCaching never304="true" />
+  </requestDispatcher>
+
+  <!-- Request Handlers
+
+       http://wiki.apache.org/solr/SolrRequestHandler
+
+       Incoming queries will be dispatched to a specific handler by name
+       based on the path specified in the request.
+
+       If a Request Handler is declared with startup="lazy", then it will
+       not be initialized until the first request that uses it.
+
+    -->
+  <!-- SearchHandler
+
+       http://wiki.apache.org/solr/SearchHandler
+
+       For processing Search Queries, the primary Request Handler
+       provided with Solr is "SearchHandler" It delegates to a sequent
+       of SearchComponents (see below) and supports distributed
+       queries across multiple shards
+    -->
+  <requestHandler name="/select" class="solr.SearchHandler">
+    <!-- default values for query parameters can be specified, these
+         will be overridden by parameters in the request
+      -->
+    <lst name="defaults">
+      <str name="echoParams">explicit</str>
+      <int name="rows">10</int>
+    </lst>
+  </requestHandler>
+
+  <initParams path="/update/**,/select">
+    <lst name="defaults">
+      <str name="df">_text_</str>
+    </lst>
+  </initParams>
+
+  <!-- Response Writers
+
+       http://wiki.apache.org/solr/QueryResponseWriter
+
+       Request responses will be written using the writer specified by
+       the 'wt' request parameter matching the name of a registered
+       writer.
+
+       The "default" writer is the default and will be used if 'wt' is
+       not specified in the request.
+    -->
+  <queryResponseWriter name="xml"
+                       default="true"
+                       class="solr.XMLResponseWriter" />
+</config>

+ 49 - 0
data/Dockerfiles/solr/solr-schema-7.7.0.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<schema name="dovecot-fts" version="2.0">
+  <fieldType name="string" class="solr.StrField" omitNorms="true" sortMissingLast="true"/>
+  <fieldType name="long" class="solr.LongPointField" positionIncrementGap="0"/>
+  <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/>
+
+  <fieldType name="text" class="solr.TextField" autoGeneratePhraseQueries="true" positionIncrementGap="100">
+    <analyzer type="index">
+      <tokenizer class="solr.StandardTokenizerFactory"/>
+      <filter class="solr.EdgeNGramFilterFactory" minGramSize="3" maxGramSize="20"/>
+      <filter class="solr.StopFilterFactory" words="stopwords.txt" ignoreCase="true"/>
+      <filter class="solr.WordDelimiterGraphFilterFactory" catenateNumbers="1" generateNumberParts="1" splitOnCaseChange="1" generateWordParts="1" splitOnNumerics="1" catenateAll="1" catenateWords="1"/>
+      <filter class="solr.FlattenGraphFilterFactory"/>
+      <filter class="solr.LowerCaseFilterFactory"/>
+      <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
+      <filter class="solr.PorterStemFilterFactory"/>
+    </analyzer>
+    <analyzer type="query">
+      <tokenizer class="solr.StandardTokenizerFactory"/>
+      <filter class="solr.SynonymGraphFilterFactory" expand="true" ignoreCase="true" synonyms="synonyms.txt"/>
+      <filter class="solr.FlattenGraphFilterFactory"/>
+      <filter class="solr.StopFilterFactory" words="stopwords.txt" ignoreCase="true"/>
+      <filter class="solr.WordDelimiterGraphFilterFactory" catenateNumbers="1" generateNumberParts="1" splitOnCaseChange="1" generateWordParts="1" splitOnNumerics="1" catenateAll="1" catenateWords="1"/>
+      <filter class="solr.LowerCaseFilterFactory"/>
+      <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/>
+      <filter class="solr.PorterStemFilterFactory"/>
+    </analyzer>
+  </fieldType>
+
+  <field name="id" type="string" indexed="true" required="true" stored="true"/>
+  <field name="uid" type="long" indexed="true" required="true" stored="true"/>
+  <field name="box" type="string" indexed="true" required="true" stored="true"/>
+  <field name="user" type="string" indexed="true" required="true" stored="true"/>
+
+  <field name="hdr" type="text" indexed="true" stored="false"/>
+  <field name="body" type="text" indexed="true" stored="false"/>
+
+  <field name="from" type="text" indexed="true" stored="false"/>
+  <field name="to" type="text" indexed="true" stored="false"/>
+  <field name="cc" type="text" indexed="true" stored="false"/>
+  <field name="bcc" type="text" indexed="true" stored="false"/>
+  <field name="subject" type="text" indexed="true" stored="false"/>
+
+  <!-- Used by Solr internally: -->
+  <field name="_version_" type="long" indexed="true" stored="true"/>
+
+  <uniqueKey>id</uniqueKey>
+</schema>

+ 21 - 17
data/Dockerfiles/watchdog/watchdog.sh

@@ -37,7 +37,7 @@ progress() {
 log_msg() {
 log_msg() {
   if [[ ${2} != "no_redis" ]]; then
   if [[ ${2} != "no_redis" ]]; then
     redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
     redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
-      tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null
+      tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null
   fi
   fi
   echo $(date) $(printf '%s\n' "${1}")
   echo $(date) $(printf '%s\n' "${1}")
 }
 }
@@ -115,7 +115,7 @@ nginx_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/nginx-mailcow
+    touch /tmp/nginx-mailcow; echo "$(tail -50 /tmp/nginx-mailcow)" > /tmp/nginx-mailcow
     host_ip=$(get_container_ip nginx-mailcow)
     host_ip=$(get_container_ip nginx-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
     /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u / -p 8081 2>> /tmp/nginx-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u / -p 8081 2>> /tmp/nginx-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -140,7 +140,7 @@ unbound_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/unbound-mailcow
+    touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
     host_ip=$(get_container_ip unbound-mailcow)
     host_ip=$(get_container_ip unbound-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
     /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -172,7 +172,7 @@ mysql_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/mysql-mailcow
+    touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
     host_ip=$(get_container_ip mysql-mailcow)
     host_ip=$(get_container_ip mysql-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
     /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -198,7 +198,7 @@ sogo_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/sogo-mailcow
+    touch /tmp/sogo-mailcow; echo "$(tail -50 /tmp/sogo-mailcow)" > /tmp/sogo-mailcow
     host_ip=$(get_container_ip sogo-mailcow)
     host_ip=$(get_container_ip sogo-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
     /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -223,7 +223,7 @@ postfix_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/postfix-mailcow
+    touch /tmp/postfix-mailcow; echo "$(tail -50 /tmp/postfix-mailcow)" > /tmp/postfix-mailcow
     host_ip=$(get_container_ip postfix-mailcow)
     host_ip=$(get_container_ip postfix-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${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 2>> /tmp/postfix-mailcow 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 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -249,7 +249,7 @@ clamd_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/clamd-mailcow
+    touch /tmp/clamd-mailcow; echo "$(tail -50 /tmp/clamd-mailcow)" > /tmp/clamd-mailcow
     host_ip=$(get_container_ip clamd-mailcow)
     host_ip=$(get_container_ip clamd-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
     /usr/lib/nagios/plugins/check_clamd -4 -H ${host_ip} 2>> /tmp/clamd-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_clamd -4 -H ${host_ip} 2>> /tmp/clamd-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -274,7 +274,7 @@ dovecot_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/dovecot-mailcow
+    touch /tmp/dovecot-mailcow; echo "$(tail -50 /tmp/dovecot-mailcow)" > /tmp/dovecot-mailcow
     host_ip=$(get_container_ip dovecot-mailcow)
     host_ip=$(get_container_ip dovecot-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${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" 2>> /tmp/dovecot-mailcow 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" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -303,7 +303,7 @@ phpfpm_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/php-fpm-mailcow
+    touch /tmp/php-fpm-mailcow; echo "$(tail -50 /tmp/php-fpm-mailcow)" > /tmp/php-fpm-mailcow
     host_ip=$(get_container_ip php-fpm-mailcow)
     host_ip=$(get_container_ip php-fpm-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
     /usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9001 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9001 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
@@ -358,10 +358,11 @@ ipv6nat_checks() {
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     err_c_cur=${err_count}
     err_c_cur=${err_count}
-    IPV6NAT_CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\")) | .id")
+    CONTAINERS=$(curl --silent --insecure https://dockerapi/containers/json)
+    IPV6NAT_CONTAINER_ID=$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\")) | .id")
     if [[ ! -z ${IPV6NAT_CONTAINER_ID} ]]; then
     if [[ ! -z ${IPV6NAT_CONTAINER_ID} ]]; then
-      LATEST_STARTED="$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\") | not)" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)"
-      LATEST_IPV6NAT="$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\"))" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)"
+      LATEST_STARTED="$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\") | not)" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)"
+      LATEST_IPV6NAT="$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\"))" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)"
       DIFFERENCE_START_TIME=$(expr ${LATEST_IPV6NAT} - ${LATEST_STARTED} 2>/dev/null)
       DIFFERENCE_START_TIME=$(expr ${LATEST_IPV6NAT} - ${LATEST_STARTED} 2>/dev/null)
       if [[ "${DIFFERENCE_START_TIME}" -lt 30 ]]; then
       if [[ "${DIFFERENCE_START_TIME}" -lt 30 ]]; then
         err_count=$(( ${err_count} + 1 ))
         err_count=$(( ${err_count} + 1 ))
@@ -375,12 +376,13 @@ ipv6nat_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep 3600
+      sleep 300
     fi
     fi
   done
   done
   return 1
   return 1
 }
 }
 
 
+
 rspamd_checks() {
 rspamd_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
@@ -388,15 +390,14 @@ rspamd_checks() {
   # Reduce error count by 2 after restarting an unhealthy container
   # Reduce error count by 2 after restarting an unhealthy container
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
-    cat /dev/null > /tmp/rspamd-mailcow
+    touch /tmp/rspamd-mailcow; echo "$(tail -50 /tmp/rspamd-mailcow)" > /tmp/rspamd-mailcow
     host_ip=$(get_container_ip rspamd-mailcow)
     host_ip=$(get_container_ip rspamd-mailcow)
     err_c_cur=${err_count}
     err_c_cur=${err_count}
-    SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan -d '
-To: null@localhost
+    SCORE=$(echo 'To: null@localhost
 From: watchdog@localhost
 From: watchdog@localhost
 
 
 Empty
 Empty
-' | jq -rc .required_score)
+' | usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .required_score)
     if [[ ${SCORE} != "9999" ]]; then
     if [[ ${SCORE} != "9999" ]]; then
       echo "Rspamd settings check failed" 2>> /tmp/rspamd-mailcow 1>&2
       echo "Rspamd settings check failed" 2>> /tmp/rspamd-mailcow 1>&2
       err_count=$(( ${err_count} + 1))
       err_count=$(( ${err_count} + 1))
@@ -561,6 +562,9 @@ while true; do
   CONTAINER_ID=
   CONTAINER_ID=
   HAS_INITDB=
   HAS_INITDB=
   read com_pipe_answer </tmp/com_pipe
   read com_pipe_answer </tmp/com_pipe
+  if [ -s "/tmp/${com_pipe_answer}" ]; then
+    cat "/tmp/${com_pipe_answer}"
+  fi
   if [[ ${com_pipe_answer} == "ratelimit" ]]; then
   if [[ ${com_pipe_answer} == "ratelimit" ]]; then
     log_msg "At least one ratelimit was applied"
     log_msg "At least one ratelimit was applied"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "No further information available."
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "No further information available."

+ 6 - 6
data/conf/dovecot/dovecot.conf

@@ -308,7 +308,7 @@ plugin {
   acl = vfile
   acl = vfile
   fts = solr
   fts = solr
   fts_autoindex = yes
   fts_autoindex = yes
-  fts_solr = url=http://solr:8983/solr/dovecot/
+  fts_solr = url=http://solr:8983/solr/dovecot-fts/
   quota = dict:Userquota::proxy::sqlquota
   quota = dict:Userquota::proxy::sqlquota
   quota_rule2 = Trash:storage=+100%%
   quota_rule2 = Trash:storage=+100%%
   sieve = /var/vmail/sieve/%u.sieve
   sieve = /var/vmail/sieve/%u.sieve
@@ -328,7 +328,7 @@ plugin {
   quota_warning = storage=95%% quota-warning 95 %u
   quota_warning = storage=95%% quota-warning 95 %u
   quota_warning2 = storage=80%% quota-warning 80 %u
   quota_warning2 = storage=80%% quota-warning 80 %u
   sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
   sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
-  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute +vacation-seconds
+  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
   sieve_extensions = +notify +imapflags +vacation-seconds
   sieve_extensions = +notify +imapflags +vacation-seconds
   sieve_max_script_size = 1M
   sieve_max_script_size = 1M
   sieve_max_redirects = 30
   sieve_max_redirects = 30
@@ -384,10 +384,10 @@ service stats {
   }
   }
 }
 }
 imap_max_line_length = 2 M
 imap_max_line_length = 2 M
-auth_cache_verify_password_with_worker = yes
-auth_cache_negative_ttl = 0
-auth_cache_ttl = 30 s
-auth_cache_size = 2 M
+#auth_cache_verify_password_with_worker = yes
+#auth_cache_negative_ttl = 0
+#auth_cache_ttl = 30 s
+#auth_cache_size = 2 M
 !include_try /usr/local/etc/dovecot/extra.conf
 !include_try /usr/local/etc/dovecot/extra.conf
 !include_try /usr/local/etc/dovecot/sogo-sso.conf
 !include_try /usr/local/etc/dovecot/sogo-sso.conf
 default_client_limit = 10400
 default_client_limit = 10400

+ 1 - 0
data/conf/postfix/local_transport

@@ -0,0 +1 @@
+/localhost$/  local:

+ 9 - 5
data/conf/postfix/main.cf

@@ -94,12 +94,16 @@ smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
 smtpd_tls_eecdh_grade = auto
 smtpd_tls_eecdh_grade = auto
 smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
 smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
 smtpd_tls_loglevel = 1
 smtpd_tls_loglevel = 1
-smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
+
+smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
 smtp_tls_protocols = !SSLv2, !SSLv3
 smtp_tls_protocols = !SSLv2, !SSLv3
-lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3
-lmtp_tls_protocols = !SSLv2, !SSLv2, !SSLv3
-smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
+
+lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+lmtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+
+smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
 smtpd_tls_protocols = !SSLv2, !SSLv3
 smtpd_tls_protocols = !SSLv2, !SSLv3
+
 smtpd_tls_security_level = may
 smtpd_tls_security_level = may
 tls_preempt_cipherlist = yes
 tls_preempt_cipherlist = yes
 tls_ssl_options = NO_COMPRESSION
 tls_ssl_options = NO_COMPRESSION
@@ -134,5 +138,5 @@ smtp_sasl_mechanism_filter = plain, login
 smtp_tls_policy_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
 smtp_tls_policy_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
 smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
 smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
 mail_name = Postcow
 mail_name = Postcow
-transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
+transport_maps = pcre:/opt/postfix/conf/local_transport, proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
 smtp_sasl_auth_soft_bounce = no
 smtp_sasl_auth_soft_bounce = no

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

@@ -2,14 +2,17 @@ smtp       inet  n       -       n       -       1       postscreen
 smtpd      pass  -       -       n       -       -       smtpd
 smtpd      pass  -       -       n       -       -       smtpd
   -o smtpd_helo_restrictions=permit_mynetworks,reject_non_fqdn_helo_hostname
   -o smtpd_helo_restrictions=permit_mynetworks,reject_non_fqdn_helo_hostname
   -o smtpd_sasl_auth_enable=no
   -o smtpd_sasl_auth_enable=no
+  -o smtpd_sender_restrictions=permit_mynetworks,reject_unlisted_sender,reject_unknown_sender_domain
 smtps    inet  n       -       n       -       -       smtpd
 smtps    inet  n       -       n       -       -       smtpd
   -o smtpd_tls_wrappermode=yes
   -o smtpd_tls_wrappermode=yes
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
+  -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3
   -o tls_preempt_cipherlist=yes
   -o tls_preempt_cipherlist=yes
 submission inet n       -       n       -       -       smtpd
 submission inet n       -       n       -       -       smtpd
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
   -o smtpd_enforce_tls=yes
   -o smtpd_enforce_tls=yes
   -o smtpd_tls_security_level=encrypt
   -o smtpd_tls_security_level=encrypt
+  -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3
   -o tls_preempt_cipherlist=yes
   -o tls_preempt_cipherlist=yes
 588 inet n      -       n       -       -       smtpd
 588 inet n      -       n       -       -       smtpd
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
   -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject

+ 30 - 32
data/conf/rspamd/dynmaps/settings.php

@@ -6,6 +6,8 @@ then any of these will trigger the rule. If a rule is triggered then no more rul
 */
 */
 header('Content-Type: text/plain');
 header('Content-Type: text/plain');
 require_once "vars.inc.php";
 require_once "vars.inc.php";
+// Getting headers sent by the client.
+//$headers = apache_request_headers();
 
 
 ini_set('error_reporting', 0);
 ini_set('error_reporting', 0);
 
 
@@ -25,6 +27,23 @@ catch (PDOException $e) {
   exit;
   exit;
 }
 }
 
 
+// Check if db changed and return header
+/*$stmt = $pdo->prepare("SELECT UNIX_TIMESTAMP(UPDATE_TIME) AS `db_update_time` FROM information_schema.tables
+  WHERE `TABLE_NAME` = 'filterconf'
+    AND TABLE_SCHEMA = :dbname;");
+$stmt->execute(array(
+  ':dbname' => $database_name
+));
+$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time'];
+
+if (isset($headers['If-Modified-Since']) && (strtotime($headers['If-Modified-Since']) == $db_update_time)) {
+  header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304);
+  exit;
+} else {
+  header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200);
+}
+*/
+
 function parse_email($email) {
 function parse_email($email) {
   if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
   if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
   $a = strrpos($email, '@');
   $a = strrpos($email, '@');
@@ -107,8 +126,8 @@ function ucl_rcpts($object, $type) {
 settings {
 settings {
   watchdog {
   watchdog {
     priority = 10;
     priority = 10;
-    rcpt = "/null@localhost/i";
-    from = "/watchdog@localhost/i";
+    rcpt_mime = "/null@localhost/i";
+    from_mime = "/watchdog@localhost/i";
     apply "default" {
     apply "default" {
       actions {
       actions {
         reject = 9999.0;
         reject = 9999.0;
@@ -199,12 +218,13 @@ while ($row = array_shift($rows)) {
 ?>
 ?>
   whitelist_<?=$username_sane;?> {
   whitelist_<?=$username_sane;?> {
 <?php
 <?php
+  $list_items = array();
   $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
   $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
     WHERE `object`= :object
     WHERE `object`= :object
       AND `option` = 'whitelist_from'");
       AND `option` = 'whitelist_from'");
   $stmt->execute(array(':object' => $row['object']));
   $stmt->execute(array(':object' => $row['object']));
   $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
-  while ($item = array_shift($list_items)) {
+  foreach ($list_items as $item) {
 ?>
 ?>
     from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
     from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
 <?php
 <?php
@@ -237,24 +257,13 @@ while ($row = array_shift($rows)) {
       "MAILCOW_WHITE"
       "MAILCOW_WHITE"
     ]
     ]
   }
   }
-  whitelist_header_<?=$username_sane;?> {
+  whitelist_mime_<?=$username_sane;?> {
 <?php
 <?php
-  $header_from = array();
-  $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
-    WHERE `object`= :object
-      AND `option` = 'whitelist_from'");
-  $stmt->execute(array(':object' => $row['object']));
-  $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  foreach ($list_items as $item) {
 ?>
 ?>
-    header = {
+    from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
 <?php
 <?php
-  while ($item = array_shift($list_items)) {
-    $header_from[] = str_replace('\*', '.*', preg_quote($item['value'], '/'));
   }
   }
-?>
-      "From" = "/(<?=implode('|', $header_from);?>)/i";
-    }
-<?php
   if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
   if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 ?>
 ?>
     priority = 5;
     priority = 5;
@@ -297,13 +306,13 @@ while ($row = array_shift($rows)) {
 ?>
 ?>
   blacklist_<?=$username_sane;?> {
   blacklist_<?=$username_sane;?> {
 <?php
 <?php
-  $items[] = array();
+  $list_items = array();
   $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
   $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
     WHERE `object`= :object
     WHERE `object`= :object
       AND `option` = 'blacklist_from'");
       AND `option` = 'blacklist_from'");
   $stmt->execute(array(':object' => $row['object']));
   $stmt->execute(array(':object' => $row['object']));
   $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
-  while ($item = array_shift($list_items)) {
+  foreach ($list_items as $item) {
 ?>
 ?>
     from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
     from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
 <?php
 <?php
@@ -338,22 +347,11 @@ while ($row = array_shift($rows)) {
   }
   }
   blacklist_header_<?=$username_sane;?> {
   blacklist_header_<?=$username_sane;?> {
 <?php
 <?php
-  $header_from = array();
-  $stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
-    WHERE `object`= :object
-      AND `option` = 'blacklist_from'");
-  $stmt->execute(array(':object' => $row['object']));
-  $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  foreach ($list_items as $item) {
 ?>
 ?>
-    header = {
+    from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
 <?php
 <?php
-  while ($item = array_shift($list_items)) {
-    $header_from[] = str_replace('\*', '.*', preg_quote($item['value'], '/'));
   }
   }
-?>
-      "From" = "/(<?=implode('|', $header_from);?>)/i";
-    }
-<?php
   if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
   if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 ?>
 ?>
     priority = 5;
     priority = 5;

+ 0 - 16
data/conf/rspamd/local.d/rspamd.conf.local

@@ -1,16 +0,0 @@
-# rspamd.conf.local
-
-worker "fuzzy" {
-  # Socket to listen on (UDP and TCP from rspamd 1.3)
-  bind_socket = "*:11445";
-  allow_update = ["127.0.0.1", "::1"];
-  # Number of processes to serve this storage (useful for read scaling)
-  count = 2;
-  # Backend ("sqlite" or "redis" - default "sqlite")
-  backend = "redis";
-  # Hashes storage time (3 months)
-  expire = 90d;
-  # Synchronize updates to the storage each minute
-  sync = 1min;
-}
-

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

@@ -84,6 +84,9 @@ $rcpt_final_mailboxes = array();
 
 
 // Loop through all rcpts
 // Loop through all rcpts
 foreach (json_decode($rcpts, true) as $rcpt) {
 foreach (json_decode($rcpts, true) as $rcpt) {
+  // Remove tag
+  $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+  
   // Break rcpt into local part and domain part
   // Break rcpt into local part and domain part
   $parsed_rcpt = parse_email($rcpt);
   $parsed_rcpt = parse_email($rcpt);
   
   

+ 12 - 0
data/conf/rspamd/override.d/worker-fuzzy.inc

@@ -0,0 +1,12 @@
+# Socket to listen on (UDP and TCP from rspamd 1.3)
+bind_socket = "*:11445";
+allow_update = ["127.0.0.1", "::1"];
+# Number of processes to serve this storage (useful for read scaling)
+count = 2;
+# Backend ("sqlite" or "redis" - default "sqlite")
+backend = "redis";
+# Hashes storage time (3 months)
+expire = 90d;
+# Synchronize updates to the storage each minute
+sync = 1min;
+

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

@@ -1,6 +1,6 @@
 bind_socket = "rspamd:9900";
 bind_socket = "rspamd:9900";
 milter = true;
 milter = true;
-upstream {
+upstream "local" {
   name = "localhost";
   name = "localhost";
   default = true;
   default = true;
   hosts = "rspamd:11333"
   hosts = "rspamd:11333"

+ 1 - 0
data/web/admin.php

@@ -746,6 +746,7 @@ $tfa_data = get_tfa();
       <div id="active_settings_map" class="collapse" >
       <div id="active_settings_map" class="collapse" >
         <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="settings_map" readonly><?=file_get_contents('http://nginx:8081/settings.php');?></textarea>
         <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="settings_map" readonly><?=file_get_contents('http://nginx:8081/settings.php');?></textarea>
       </div>
       </div>
+      <br>
       <?php $rsettings = rsettings('get'); ?>
       <?php $rsettings = rsettings('get'); ?>
         <form class="form" data-id="rsettings" role="form" method="post">
         <form class="form" data-id="rsettings" role="form" method="post">
           <div class="row">
           <div class="row">

File diff suppressed because it is too large
+ 5 - 4
data/web/css/build/001-bootstrap.min.css


+ 13 - 0
data/web/inc/ajax/qr_gen.php

@@ -0,0 +1,13 @@
+<?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['token']) && ctype_alnum($_GET['token'])) {
+  echo $tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $totp_secret);
+}
+
+?>

+ 3 - 0
data/web/inc/ajax/transport_check.php

@@ -58,6 +58,9 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
       )
       )
     );
     );
     $mail->SMTPDebug = 3;
     $mail->SMTPDebug = 3;
+    if ($port == 465) {
+      $mail->SMTPSecure = "ssl";
+    }
     $mail->Debugoutput = function($str, $level) {
     $mail->Debugoutput = function($str, $level) {
       foreach(preg_split("/((\r?\n)|(\r\n?)|\n)/", $str) as $line){
       foreach(preg_split("/((\r?\n)|(\r\n?)|\n)/", $str) as $line){
         if (empty($line)) { continue; }
         if (empty($line)) { continue; }

+ 9 - 0
data/web/inc/footer.inc.php

@@ -93,6 +93,15 @@ $(document).ready(function() {
     }
     }
     if ($(this).val() == "totp") {
     if ($(this).val() == "totp") {
       $('#TOTPModal').modal('show');
       $('#TOTPModal').modal('show');
+      request_token = $('#tfa-qr-img').data('totp-secret');
+      $.ajax({
+        url: '/inc/ajax/qr_gen.php',
+        data: {
+          token: request_token,
+        },
+      }).done(function (result) {
+        $("#tfa-qr-img").attr("src", result);
+      });
       $("option:selected").prop("selected", false);
       $("option:selected").prop("selected", false);
     }
     }
     if ($(this).val() == "u2f") {
     if ($(this).val() == "u2f") {

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

@@ -1477,7 +1477,7 @@ function solr_status() {
   $endpoint = 'http://solr:8983/solr/admin/cores';
   $endpoint = 'http://solr:8983/solr/admin/cores';
   $params = array(
   $params = array(
     'action' => 'STATUS',
     'action' => 'STATUS',
-    'core' => 'dovecot',
+    'core' => 'dovecot-fts',
     'indexInfo' => 'true'
     'indexInfo' => 'true'
   );
   );
   $url = $endpoint . '?' . http_build_query($params);
   $url = $endpoint . '?' . http_build_query($params);
@@ -1494,7 +1494,7 @@ function solr_status() {
   else {
   else {
     curl_close($curl);
     curl_close($curl);
     $status = json_decode($response, true);
     $status = json_decode($response, true);
-    return (!empty($status['status']['dovecot'])) ? $status['status']['dovecot'] : false;
+    return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
   }
   }
   return false;
   return false;
 }
 }

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

@@ -561,7 +561,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'msg' => array('is_alias_or_mailbox', htmlspecialchars($address))
                 'msg' => array('is_alias_or_mailbox', htmlspecialchars($address))
               );
               );
-              return false;
+              continue;
             }
             }
             $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
             $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
               WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
               WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
@@ -573,7 +573,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'msg' => array('domain_not_found', htmlspecialchars($domain))
                 'msg' => array('domain_not_found', htmlspecialchars($domain))
               );
               );
-              return false;
+              continue;
             }
             }
             $stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
             $stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
               WHERE `address`= :address");
               WHERE `address`= :address");
@@ -585,7 +585,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'msg' => array('is_spam_alias', htmlspecialchars($address))
                 'msg' => array('is_spam_alias', htmlspecialchars($address))
               );
               );
-              return false;
+              continue;
             }
             }
             if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
             if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
@@ -593,7 +593,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'msg' => 'alias_invalid'
                 'msg' => 'alias_invalid'
               );
               );
-              return false;
+              continue;
             }
             }
             if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
             if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
@@ -601,7 +601,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'msg' => 'access_denied'
                 'msg' => 'access_denied'
               );
               );
-              return false;
+              continue;
             }
             }
             $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `active`)
             $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `active`)
               VALUES (:address, :public_comment, :private_comment, :goto, :domain, :active)");
               VALUES (:address, :public_comment, :private_comment, :goto, :domain, :active)");
@@ -755,7 +755,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           }
           $password     = $_data['password'];
           $password     = $_data['password'];
           $password2    = $_data['password2'];
           $password2    = $_data['password2'];
-          $name         = $_data['name'];
+          $name         = ltrim(rtrim($_data['name'], '>'), '<');
           $quota_m			= filter_var($_data['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
           $quota_m			= filter_var($_data['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
           if (empty($name)) {
           if (empty($name)) {
             $name = $local_part;
             $name = $local_part;
@@ -1993,7 +1993,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $active     = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
               $active     = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
               (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
               (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
               (int)$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
               (int)$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
-              $name       = (!empty($_data['name'])) ? $_data['name'] : $is_now['name'];
+              $name       = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
               $domain     = $is_now['domain'];
               $domain     = $is_now['domain'];
               $quota_m    = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['quota'] / 1048576);
               $quota_m    = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['quota'] / 1048576);
               $quota_b    = $quota_m * 1048576;
               $quota_b    = $quota_m * 1048576;
@@ -3525,7 +3525,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
             }
             if (strtolower(getenv('SKIP_SOLR')) == 'n') {
             if (strtolower(getenv('SKIP_SOLR')) == 'n') {
               $curl = curl_init();
               $curl = curl_init();
-              curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot/update?commit=true');
+              curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true');
               curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml'));
               curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml'));
               curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
               curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
               curl_setopt($curl, CURLOPT_POST, 1);
               curl_setopt($curl, CURLOPT_POST, 1);
@@ -3714,7 +3714,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
       }
       }
     break;
     break;
   }
   }
-  if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox'))) {
+  if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
     update_sogo_static_view();
     update_sogo_static_view();
   }
   }
 }
 }

+ 2 - 1
data/web/inc/prerequisites.inc.php

@@ -36,7 +36,8 @@ foreach ($css_dir as $css_file) {
 
 
 // U2F API + T/HOTP API
 // U2F API + T/HOTP API
 $u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']);
 $u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']);
-$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL);
+$qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
+$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
 
 
 // Redis
 // Redis
 $redis = new Redis();
 $redis = new Redis();

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

@@ -141,7 +141,7 @@ $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false;
 // Force password change on next login (only allows login to mailcow UI)
 // Force password change on next login (only allows login to mailcow UI)
 $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
 $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
 
 
-// Force password change on next login (only allows login to mailcow UI)
+// Enable SOGo access (set to false to disable access by default)
 $MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
 $MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
 
 
 // Send notification when quarantine is not empty (never, hourly, daily, weekly)
 // Send notification when quarantine is not empty (never, hourly, daily, weekly)

+ 31 - 3
data/web/js/site/mailbox.js

@@ -169,7 +169,25 @@ $(document).ready(function() {
     // $("#active-script").closest('td').css('background-color','#b0f0a0');
     // $("#active-script").closest('td').css('background-color','#b0f0a0');
     // $("#inactive-script").closest('td').css('background-color','#b0f0a0');
     // $("#inactive-script").closest('td').css('background-color','#b0f0a0');
   // });
   // });
-
+  $('#addResourceModal').on('shown.bs.modal', function() {
+    $("#multiple_bookings").val($("#multiple_bookings_select").val());
+    if ($("#multiple_bookings").val() == "custom") {
+      $("#multiple_bookings_custom_div").show();
+      $("#multiple_bookings").val($("#multiple_bookings_custom").val());
+    }
+  })
+  $("#multiple_bookings_select").change(function() {
+    $("#multiple_bookings").val($("#multiple_bookings_select").val());
+    if ($("#multiple_bookings").val() == "custom") {
+      $("#multiple_bookings_custom_div").show();
+    }
+    else {
+      $("#multiple_bookings_custom_div").hide();
+    }
+  });
+  $("#multiple_bookings_custom").bind ("change keypress keyup blur", function () {
+    $("#multiple_bookings").val($("#multiple_bookings_custom").val());
+  });
 
 
 
 
 });
 });
@@ -734,8 +752,18 @@ jQuery(function($){
               '</div>';
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
             item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />';
             item.goto = escapeHtml(item.goto.replace(/,/g, " "));
             item.goto = escapeHtml(item.goto.replace(/,/g, " "));
-            item.public_comment = escapeHtml(item.public_comment);
-            item.private_comment = escapeHtml(item.private_comment);
+            if (item.public_comment !== null) {
+              item.public_comment = escapeHtml(item.public_comment);
+            }
+            else {
+              item.public_comment = '-';
+            }
+            if (item.private_comment !== null) {
+              item.private_comment = escapeHtml(item.private_comment);
+            }
+            else {
+              item.private_comment = '-';
+            }
             if (item.is_catch_all == 1) {
             if (item.is_catch_all == 1) {
               item.address = '<div class="label label-default">Catch-All</div> ' + escapeHtml(item.address);
               item.address = '<div class="label label-default">Catch-All</div> ' + escapeHtml(item.address);
             }
             }

+ 42 - 42
data/web/js/site/quarantine.js

@@ -1,11 +1,13 @@
 // Base64 functions
 // 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}};
 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($){
 jQuery(function($){
   acl_data = JSON.parse(acl);
   acl_data = JSON.parse(acl);
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
   var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
   var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
   function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
   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 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_quarantine_table() {
   function draw_quarantine_table() {
     ft_quarantinetable = FooTable.init('#quarantinetable', {
     ft_quarantinetable = FooTable.init('#quarantinetable', {
       "columns": [
       "columns": [
@@ -56,54 +58,52 @@ jQuery(function($){
       "empty": lang.empty,
       "empty": lang.empty,
       "paging": {"enabled": true,"limit": 5,"size": pagination_size},
       "paging": {"enabled": true,"limit": 5,"size": pagination_size},
       "sorting": {"enabled": true},
       "sorting": {"enabled": true},
-      "on": {
-        "ready.ft.table": btn_group_quarantine,
-        "after.ft.paging": btn_group_quarantine
-      },
       "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
       "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
     });
     });
   }
   }
 
 
-  btn_group_quarantine = 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();
-          }
-          $( "li" ).each(function( index ) {
-            console.log( index + ": " + $( this ).text() );
-          });
-          $('[data-id="qitems_single"]').each(function( index ) {
-            $(this).attr("data-item", qitem);
+  $('body').on('click', '.show_qid_info', function (e) {
+    e.preventDefault();
+    var qitem = $(this).data('item');
+    var qError = $("#qid_error");
+
+    $('#qidDetailModal').modal('show');
+    qError.hide();
+
+    $.ajax({
+      url: '/inc/ajax/qitem_details.php',
+      data: { id: qitem },
+      dataType: 'json',
+      success: function(data){
+        if (typeof data.error !== 'undefined') {
+          qError.text(data.error);
+          qError.show();
+        }
+        $('[data-id="qitems_single"]').each(function(index) {
+          $(this).attr("data-item", qitem);
+        });
+
+        $('#qid_detail_subj').text(data.subject);
+        $('#qid_detail_text').text(data.text_plain);
+        $('#qid_detail_text_from_html').text(data.text_html);
+
+        if (typeof data.attachments !== 'undefined') {
+          qAtts = $("#qid_detail_atts");
+          qAtts.text('');
+          $.each(data.attachments, function(index, value) {
+            qAtts.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>'
+            );
           });
           });
-          $('#qid_detail_subj').text(data.subject);
-          $('#qid_detail_text').text(data.text_plain);
-          $('#qid_detail_text_from_html').text(data.text_html);
-          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('-');
-          }
         }
         }
-      });
-    })
-  }
+        else {
+          qAtts.text('-');
+        }
+      }
+    });
+  });
+
   // Initial table drawings
   // Initial table drawings
   draw_quarantine_table();
   draw_quarantine_table();
 });
 });

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

@@ -607,11 +607,11 @@ $lang['admin']['forwarding_hosts_hint'] = 'Eingehende Nachrichten werden von den
 $lang['admin']['forwarding_hosts_add_hint'] = 'Sie können entweder IPv4/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.';
 $lang['admin']['forwarding_hosts_add_hint'] = 'Sie können entweder IPv4/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.';
 $lang['admin']['relayhosts_hint'] = 'Erstellen Sie senderabhängige Transporte, um diese im Einstellungsdialog einer Domain auszuwählen.<br>
 $lang['admin']['relayhosts_hint'] = 'Erstellen Sie senderabhängige Transporte, um diese im Einstellungsdialog einer Domain auszuwählen.<br>
   Der Transporttyp lautet immer "smtp:". Benutzereinstellungen bezüglich Verschlüsselungsrichtlinie werden beim Transport berücksichtigt.';
   Der Transporttyp lautet immer "smtp:". Benutzereinstellungen bezüglich Verschlüsselungsrichtlinie werden beim Transport berücksichtigt.';
-$lang['admin']['transports_hint'] = 'Transport Maps <b>überwiegen</b> senderabhängige Transport Maps und ignorieren die individuellen Einstellungen eines Benutzers bezüglich Verschlüsselungsrichtlinie, da der Absender bei Ermittlung der Transportregel nicht berücksichtigt wird.<br>
-  Der Transport erfolgt immer via "smtp:".<br>
-  Ein Eintrag in der TLS Policy Map kann eine Verschlüsselung erzwingen.<br>
-  Die Authentifizierung wird anhand des Host Parameters ermittelt, hierbei würde bei einem beispielhaften Next Hop "[host]:25" immer zuerst "host" abfragt und <b>erst im Anschluss</b> "[host]:25".<br>
-  Dieses Verhalten schließt die <b>gleichzeitige Verwendung</b> von Einträgen der Art "host" sowie "[host]:25" aus.';
+$lang['admin']['transports_hint'] = 'Transport Maps <b>überwiegen</b> senderabhängige Transport Maps.
+→ Transport Maps ignorieren Mailbox-Einstellungen für ausgehende Verschlüsselung. Eine serverweite TLS-Richtlinie wird jedoch angewendet.<br>
+→ Der Transport erfolgt immer via "smtp:".<br>
+→ Adressen, die mit "/localhost$/" übereinstimmen, werden immer via "local:" transportiert, daher sind sie von einer Zieldefinition "*" ausgeschlossen.<br>
+ Die Authentifizierung wird anhand des "Next hop" Parameters ermittelt. Hierbei würde bei einem beispielhaften Wert "[host]:25" immer zuerst "host" abfragt und <b>erst im Anschluss</b> "[host]:25". Dieses Verhalten schließt die <b>gleichzeitige Verwendung</b> von Einträgen der Art "host" sowie "[host]:25" aus.';
 $lang['admin']['add_relayhost_hint'] = 'Bitte beachten Sie, dass Anmeldedaten unverschlüsselt gespeichert werden.<br>
 $lang['admin']['add_relayhost_hint'] = 'Bitte beachten Sie, dass Anmeldedaten unverschlüsselt gespeichert werden.<br>
   Angelegte Transporte dieser Art sind <b>senderabhängig</b> und müssen erst einer Domain zugewiesen werden, bevor sie als Transport verwendet werden.<br>
   Angelegte Transporte dieser Art sind <b>senderabhängig</b> und müssen erst einer Domain zugewiesen werden, bevor sie als Transport verwendet werden.<br>
   Diese Einstellungen entsprechen demach <i>nicht</i> dem "relayhost" Parameter in Postfix.';
   Diese Einstellungen entsprechen demach <i>nicht</i> dem "relayhost" Parameter in Postfix.';

+ 5 - 3
data/web/lang/lang.en.php

@@ -631,9 +631,11 @@ $lang['admin']['forwarding_hosts_hint'] = 'Incoming messages are unconditionally
 $lang['admin']['forwarding_hosts_add_hint'] = 'You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).';
 $lang['admin']['forwarding_hosts_add_hint'] = 'You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).';
 $lang['admin']['relayhosts_hint'] = 'Define sender-dependent transports to be able to select them in a domains configuration dialog.<br>
 $lang['admin']['relayhosts_hint'] = 'Define sender-dependent transports to be able to select them in a domains configuration dialog.<br>
   The transport service is always "smtp:". A users individual outbound TLS policy setting is taken into account.';
   The transport service is always "smtp:". A users individual outbound TLS policy setting is taken into account.';
-$lang['admin']['transports_hint'] = 'A transport map entry <b>overrules</b> a sender-dependent transport map</b>.<br>
-Outbound TLS policy settings per-user are ignored and can only be enfored by TLS policy map entries. The transport service is always "smtp:".<br>
-To determine credentials for an exemplary next hop "[host]:25", Postfix <b>always</b> queries for "nexthop" before searching for "[nexthop]:25". This behavior makes it impossible to use "nexthop" and "[nexthop]:25" at the same time.';
+$lang['admin']['transports_hint'] = '→ A transport map entry <b>overrules</b> a sender-dependent transport map</b>.<br>
+→ Outbound TLS policy settings per-user are ignored and can only be enfored by TLS policy map entries.<br>
+→ The transport service for defined transports is always "smtp:".<br>
+→ Adresses matching "/localhost$/" will always be transported via "local:", therefore a "*" destination will not apply to those addresses.<br>
+→ To determine credentials for an exemplary next hop "[host]:25", Postfix <b>always</b> queries for "host" before searching for "[host]:25". This behavior makes it impossible to use "host" and "[host]:25" at the same time.';
 $lang['admin']['add_relayhost_hint'] = 'Please be aware that authentication data, if any, will be stored as plain text.';
 $lang['admin']['add_relayhost_hint'] = 'Please be aware that authentication data, if any, will be stored as plain text.';
 $lang['admin']['add_transports_hint'] = 'Please be aware that authentication data, if any, will be stored as plain text.';
 $lang['admin']['add_transports_hint'] = 'Please be aware that authentication data, if any, will be stored as plain text.';
 $lang['admin']['host'] = 'Host';
 $lang['admin']['host'] = 'Host';

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

@@ -81,7 +81,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
           <ol>
           <ol>
             <li>
             <li>
               <p><?=$lang['tfa']['scan_qr_code'];?></p>
               <p><?=$lang['tfa']['scan_qr_code'];?></p>
-              <img src="<?=$tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $totp_secret);?>">
+              <img id="tfa-qr-img" data-totp-secret="<?=$totp_secret;?>" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
               <p class="help-block"><?=$lang['tfa']['enter_qr_code'];?>:<br />
               <p class="help-block"><?=$lang['tfa']['enter_qr_code'];?>:<br />
               <code><?=$totp_secret;?></code>
               <code><?=$totp_secret;?></code>
               </p>
               </p>

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

@@ -785,24 +785,3 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
     </div>
   </div>
   </div>
 </div><!-- DNS info modal -->
 </div><!-- DNS info modal -->
-<script>
-$('#addResourceModal').on('shown.bs.modal', function() {
-  $("#multiple_bookings").val($("#multiple_bookings_select").val());
-  if ($("#multiple_bookings").val() == "custom") {
-    $("#multiple_bookings_custom_div").show();
-    $("#multiple_bookings").val($("#multiple_bookings_custom").val());
-  }
-})
-$("#multiple_bookings_select").change(function() {
-  $("#multiple_bookings").val($("#multiple_bookings_select").val());
-  if ($("#multiple_bookings").val() == "custom") {
-    $("#multiple_bookings_custom_div").show();
-  }
-  else {
-    $("#multiple_bookings_custom_div").hide();
-  }
-});
-$("#multiple_bookings_custom").bind ("change keypress keyup blur", function () {
-  $("#multiple_bookings").val($("#multiple_bookings_custom").val());
-});
-</script>

+ 26 - 18
data/web/sogo-auth.php

@@ -5,7 +5,7 @@ $ALLOW_ADMIN_EMAIL_LOGIN = (preg_match(
   $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"]
   $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"]
 ));
 ));
 
 
-$session_var_user = 'sogo-sso-user';
+$session_var_user_allowed = 'sogo-sso-user-allowed';
 $session_var_pass = 'sogo-sso-pass';
 $session_var_pass = 'sogo-sso-pass';
 
 
 // prevent if feature is disabled
 // prevent if feature is disabled
@@ -44,10 +44,10 @@ elseif (isset($_GET['login'])) {
         // load master password
         // load master password
         $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
         $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
         // register username and password in session
         // register username and password in session
-        $_SESSION[$session_var_user] = $login;
+        $_SESSION[$session_var_user_allowed][] = $login;
         $_SESSION[$session_var_pass] = $sogo_sso_pass;
         $_SESSION[$session_var_pass] = $sogo_sso_pass;
         // redirect to sogo (sogo will get the correct credentials via nginx auth_request
         // redirect to sogo (sogo will get the correct credentials via nginx auth_request
-        header("Location: /SOGo/");
+        header("Location: /SOGo/so/${login}");
         exit;
         exit;
       }
       }
     }
     }
@@ -55,24 +55,32 @@ elseif (isset($_GET['login'])) {
   header('HTTP/1.0 403 Forbidden');
   header('HTTP/1.0 403 Forbidden');
   exit;
   exit;
 }
 }
-// do not check for admin-login / sogo-sso for EAS and DAV requests, SOGo can check auth itself if no authorization header is set
+// only check for admin-login on sogo GUI requests
 elseif (
 elseif (
-  strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 28), "/Microsoft-Server-ActiveSync") !== 0 &&
-  strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/dav") !== 0
+  strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0
 ) {
 ) {
   // this is an nginx auth_request call, we check for existing sogo-sso session variables
   // this is an nginx auth_request call, we check for existing sogo-sso session variables
   session_start();
   session_start();
-  if (isset($_SESSION[$session_var_user]) && filter_var($_SESSION[$session_var_user], FILTER_VALIDATE_EMAIL)) {
-      $username = $_SESSION[$session_var_user];
-      $password = $_SESSION[$session_var_pass];
-      header("X-User: $username");
-      header("X-Auth: Basic ".base64_encode("$username:$password"));
-      header("X-Auth-Type: Basic");
-  } else {
-      // if username is empty, SOGo will display the normal login form
-      header("X-User: ");
-      header("X-Auth: ");
-      header("X-Auth-Type: ");
+  // extract email address from "/SOGo/so/user@domain/xy"
+  $url_parts = explode("/", $_SERVER['HTTP_X_ORIGINAL_URI']);
+  $email = $url_parts[3];
+  // check if this email is in session allowed list
+  if (
+      !empty($email) &&
+      filter_var($email, FILTER_VALIDATE_EMAIL) &&
+      is_array($_SESSION[$session_var_user_allowed]) &&
+      in_array($email, $_SESSION[$session_var_user_allowed])
+  ) {
+    $username = $email;
+    $password = $_SESSION[$session_var_pass];
+    header("X-User: $username");
+    header("X-Auth: Basic ".base64_encode("$username:$password"));
+    header("X-Auth-Type: Basic");
+    exit;
   }
   }
-  exit;
 }
 }
+
+// if username is empty, SOGo will use the normal login methods / login form
+header("X-User: ");
+header("X-Auth: ");
+header("X-Auth-Type: ");

+ 8 - 7
docker-compose.yml

@@ -71,7 +71,7 @@ services:
             - clamd
             - clamd
 
 
     rspamd-mailcow:
     rspamd-mailcow:
-      image: mailcow/rspamd:1.34
+      image: mailcow/rspamd:1.38
       build: ./data/Dockerfiles/rspamd
       build: ./data/Dockerfiles/rspamd
       stop_grace_period: 30s
       stop_grace_period: 30s
       depends_on:
       depends_on:
@@ -94,7 +94,7 @@ services:
             - rspamd
             - rspamd
 
 
     php-fpm-mailcow:
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.34
+      image: mailcow/phpfpm:1.35
       build: ./data/Dockerfiles/phpfpm
       build: ./data/Dockerfiles/phpfpm
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
       depends_on:
@@ -195,6 +195,7 @@ services:
         - MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-1440}
         - MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-1440}
         - ACL_ANYONE=${ACL_ANYONE:-disallow}
         - ACL_ANYONE=${ACL_ANYONE:-disallow}
         - SKIP_SOLR=${SKIP_SOLR:-y}
         - SKIP_SOLR=${SKIP_SOLR:-y}
+        - MAILDIR_SUB=${MAILDIR_SUB:-}
       ports:
       ports:
         - "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
         - "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
         - "${IMAP_PORT:-143}:143"
         - "${IMAP_PORT:-143}:143"
@@ -219,7 +220,7 @@ services:
             - dovecot
             - dovecot
 
 
     postfix-mailcow:
     postfix-mailcow:
-      image: mailcow/postfix:1.29
+      image: mailcow/postfix:1.31
       build: ./data/Dockerfiles/postfix
       build: ./data/Dockerfiles/postfix
       volumes:
       volumes:
         - ./data/conf/postfix:/opt/postfix/conf
         - ./data/conf/postfix:/opt/postfix/conf
@@ -306,7 +307,7 @@ services:
     acme-mailcow:
     acme-mailcow:
       depends_on:
       depends_on:
         - nginx-mailcow
         - nginx-mailcow
-      image: mailcow/acme:1.48
+      image: mailcow/acme:1.49
       build: ./data/Dockerfiles/acme
       build: ./data/Dockerfiles/acme
       dns:
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
         - ${IPV4_NETWORK:-172.22.1}.254
@@ -357,7 +358,7 @@ services:
         - /lib/modules:/lib/modules:ro
         - /lib/modules:/lib/modules:ro
 
 
     watchdog-mailcow:
     watchdog-mailcow:
-      image: mailcow/watchdog:1.35
+      image: mailcow/watchdog:1.37
       # Debug
       # Debug
       #command: /watchdog.sh
       #command: /watchdog.sh
       build: ./data/Dockerfiles/watchdog
       build: ./data/Dockerfiles/watchdog
@@ -405,11 +406,11 @@ services:
             - dockerapi
             - dockerapi
 
 
     solr-mailcow:
     solr-mailcow:
-      image: mailcow/solr:1.2
+      image: mailcow/solr:1.4
       build: ./data/Dockerfiles/solr
       build: ./data/Dockerfiles/solr
       restart: always
       restart: always
       volumes:
       volumes:
-        - solr-vol-1:/opt/solr/server/solr/dovecot/data
+        - solr-vol-1:/opt/solr/server/solr/dovecot-fts/data
       dns:
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
         - ${IPV4_NETWORK:-172.22.1}.254
       environment:
       environment:

+ 6 - 0
generate_config.sh

@@ -16,6 +16,7 @@ if [ -f mailcow.conf ]; then
   case $response in
   case $response in
     [yY][eE][sS]|[yY])
     [yY][eE][sS]|[yY])
       mv mailcow.conf mailcow.conf_backup
       mv mailcow.conf mailcow.conf_backup
+      chmod 600 mailcow.conf_backup
       ;;
       ;;
     *)
     *)
       exit 1
       exit 1
@@ -237,9 +238,14 @@ IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
 #API_KEY=
 #API_KEY=
 #API_ALLOW_FROM=127.0.0.1,1.2.3.4
 #API_ALLOW_FROM=127.0.0.1,1.2.3.4
 
 
+# mail_home is ~/Maildir
+MAILDIR_SUB=Maildir
+
 EOF
 EOF
 
 
 mkdir -p data/assets/ssl
 mkdir -p data/assets/ssl
 
 
+chmod 600 mailcow.conf
+
 # copy but don't overwrite existing certificate
 # copy but don't overwrite existing certificate
 cp -n data/assets/ssl-example/*.pem data/assets/ssl/
 cp -n data/assets/ssl-example/*.pem data/assets/ssl/

+ 3 - 5
helper-scripts/nextcloud.sh

@@ -76,9 +76,8 @@ elif [[ ${NC_UPDATE} == "y" ]]; then
     curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-15.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
     curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-15.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
       && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
       && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
       && rm nextcloud.tar.bz2 \
       && rm nextcloud.tar.bz2 \
-      && rm -rf ./data/web/nextcloud/updater \
+      #&& rm -rf ./data/web/nextcloud/updater \
       && mkdir -p ./data/web/nextcloud/data \
       && mkdir -p ./data/web/nextcloud/data \
-      && mkdir -p ./data/web/nextcloud/custom_apps \
       && chmod +x ./data/web/nextcloud/occ
       && chmod +x ./data/web/nextcloud/occ
     docker exec -it $(docker ps -f name=php-fpm-mailcow -q) bash -c "chown www-data:www-data -R /web/nextcloud"
     docker exec -it $(docker ps -f name=php-fpm-mailcow -q) bash -c "chown www-data:www-data -R /web/nextcloud"
     docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings upgrade"
     docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings upgrade"
@@ -106,12 +105,11 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
   curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-15.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
   curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-15.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
     && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
     && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
     && rm nextcloud.tar.bz2 \
     && rm nextcloud.tar.bz2 \
-    && rm -rf ./data/web/nextcloud/updater \
+    #&& rm -rf ./data/web/nextcloud/updater \
     && mkdir -p ./data/web/nextcloud/data \
     && mkdir -p ./data/web/nextcloud/data \
-    && mkdir -p ./data/web/nextcloud/custom_apps \
     && chmod +x ./data/web/nextcloud/occ
     && chmod +x ./data/web/nextcloud/occ
 
 
-  docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud/data /web/nextcloud/config /web/nextcloud/apps /web/nextcloud/custom_apps"
+  docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud/data /web/nextcloud/config /web/nextcloud/apps"
   docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings maintenance:install \
   docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings maintenance:install \
     --database mysql \
     --database mysql \
     --database-host mysql \
     --database-host mysql \

+ 20 - 5
update.sh

@@ -6,9 +6,12 @@ if [ "$(id -u)" -ne "0" ]; then
   exit 1
   exit 1
 fi
 fi
 
 
-#exit on error and pipefail
+# Exit on error and pipefail
 set -o pipefail
 set -o pipefail
 
 
+# Add /opt/bin to PATH
+PATH=$PATH:/opt/bin
+
 umask 0022
 umask 0022
 
 
 for bin in curl docker-compose docker git awk sha1sum; do
 for bin in curl docker-compose docker git awk sha1sum; do
@@ -68,8 +71,12 @@ while (($#)); do
   case "${1}" in
   case "${1}" in
     --check|-c)
     --check|-c)
       echo "Checking remote code for updates..."
       echo "Checking remote code for updates..."
-      git fetch origin #${BRANCH}
-      if [[ -z $(git log HEAD --pretty=format:"%H" | grep $(git rev-parse origin/${BRANCH})) ]]; then
+      LATEST_REV=$(git ls-remote --exit-code --refs --quiet https://github.com/mailcow/mailcow-dockerized ${BRANCH} | cut -f1)
+      if [ $? -ne 0 ]; then
+        echo "A problem occurred while trying to fetch the latest revision from github."
+        exit 99
+      fi
+      if [[ -z $(git log HEAD --pretty=format:"%H" | grep "${LATEST_REV}") ]]; then
         echo "Updated code is available."
         echo "Updated code is available."
         exit 0
         exit 0
       else
       else
@@ -98,6 +105,7 @@ while (($#)); do
 done
 done
 
 
 [[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
 [[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
+chmod 600 mailcow.conf
 source mailcow.conf
 source mailcow.conf
 DOTS=${MAILCOW_HOSTNAME//[^.]};
 DOTS=${MAILCOW_HOSTNAME//[^.]};
 if [ ${#DOTS} -lt 2 ]; then
 if [ ${#DOTS} -lt 2 ]; then
@@ -127,6 +135,7 @@ CONFIG_ARRAY=(
   "API_KEY"
   "API_KEY"
   "API_ALLOW_FROM"
   "API_ALLOW_FROM"
   "MAILDIR_GC_TIME"
   "MAILDIR_GC_TIME"
+  "MAILDIR_SUB"
   "ACL_ANYONE"
   "ACL_ANYONE"
   "SOLR_HEAP"
   "SOLR_HEAP"
   "SKIP_SOLR"
   "SKIP_SOLR"
@@ -236,6 +245,13 @@ for option in ${CONFIG_ARRAY[@]}; do
       echo '# Disable Solr or if you do not want to store a readable index of your mails in solr-vol-1.' >> mailcow.conf
       echo '# Disable Solr or if you do not want to store a readable index of your mails in solr-vol-1.' >> mailcow.conf
       echo "SKIP_SOLR=y" >> mailcow.conf
       echo "SKIP_SOLR=y" >> mailcow.conf
   fi
   fi
+  elif [[ ${option} == "MAILDIR_SUB" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf
+      echo "#MAILDIR_SUB=Maildir" >> mailcow.conf
+      echo "MAILDIR_SUB=" >> mailcow.conf
+  fi
   elif ! grep -q ${option} mailcow.conf; then
   elif ! grep -q ${option} mailcow.conf; then
     echo "Adding new option \"${option}\" to mailcow.conf"
     echo "Adding new option \"${option}\" to mailcow.conf"
     echo "${option}=n" >> mailcow.conf
     echo "${option}=n" >> mailcow.conf
@@ -352,9 +368,8 @@ if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then
   read -p "Press any key to continue..." < /dev/tty
   read -p "Press any key to continue..." < /dev/tty
 fi
 fi
 
 
-echo -e "Fixing project name... "
+# Checking for old project name bug
 sed -i 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf
 sed -i 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf
-sed -i '/COMPOSE_PROJECT_NAME=/s/-//g' mailcow.conf
 
 
 echo -e "Fixing PHP-FPM worker ports for Nginx sites..."
 echo -e "Fixing PHP-FPM worker ports for Nginx sites..."
 sed -i 's#phpfpm:9000#phpfpm:9002#g' data/conf/nginx/*.conf
 sed -i 's#phpfpm:9000#phpfpm:9002#g' data/conf/nginx/*.conf

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