Browse Source

Merge pull request #2 from mailcow/master

update local mailcow
Zekeriya Akgül 6 years ago
parent
commit
48081ff5b7
100 changed files with 3165 additions and 1956 deletions
  1. 53 18
      .github/ISSUE_TEMPLATE/Bug_report.md
  2. 6 0
      .gitignore
  3. 1 1
      README.md
  4. 6 5
      data/Dockerfiles/acme/Dockerfile
  5. 54 31
      data/Dockerfiles/acme/docker-entrypoint.sh
  6. 1 1
      data/Dockerfiles/clamd/Dockerfile
  7. 4 1
      data/Dockerfiles/clamd/bootstrap.sh
  8. 8 7
      data/Dockerfiles/dockerapi/Dockerfile
  9. 314 315
      data/Dockerfiles/dockerapi/server.py
  10. 61 66
      data/Dockerfiles/dovecot/Dockerfile
  11. 18 0
      data/Dockerfiles/dovecot/clean_q_aged.sh
  12. 97 31
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  13. 296 187
      data/Dockerfiles/dovecot/imapsync
  14. 45 30
      data/Dockerfiles/dovecot/imapsync_cron.pl
  15. 2 1
      data/Dockerfiles/dovecot/quarantine_notify.py
  16. 1 1
      data/Dockerfiles/dovecot/quota_notify.py
  17. 33 17
      data/Dockerfiles/dovecot/sa-rules.sh
  18. 1 1
      data/Dockerfiles/dovecot/supervisord.conf
  19. 2 2
      data/Dockerfiles/dovecot/syslog-ng.conf
  20. 1 1
      data/Dockerfiles/dovecot/trim_logs.sh
  21. 8 5
      data/Dockerfiles/netfilter/Dockerfile
  22. 202 117
      data/Dockerfiles/netfilter/server.py
  23. 19 0
      data/Dockerfiles/olefy/Dockerfile
  24. 6 5
      data/Dockerfiles/phpfpm/Dockerfile
  25. 33 14
      data/Dockerfiles/phpfpm/docker-entrypoint.sh
  26. 8 12
      data/Dockerfiles/postfix/Dockerfile
  27. 43 18
      data/Dockerfiles/postfix/postfix.sh
  28. 5 0
      data/Dockerfiles/postfix/supervisord.conf
  29. 2 1
      data/Dockerfiles/postfix/syslog-ng.conf
  30. 0 9
      data/Dockerfiles/postfix/zeyple.conf
  31. 0 274
      data/Dockerfiles/postfix/zeyple.py
  32. 16 14
      data/Dockerfiles/rspamd/Dockerfile
  33. 32 4
      data/Dockerfiles/rspamd/docker-entrypoint.sh
  34. 6 13
      data/Dockerfiles/sogo/Dockerfile
  35. 42 17
      data/Dockerfiles/sogo/bootstrap-sogo.sh
  36. 19 3
      data/Dockerfiles/solr/Dockerfile
  37. 19 378
      data/Dockerfiles/solr/docker-entrypoint.sh
  38. 289 0
      data/Dockerfiles/solr/solr-config-7.7.0.xml
  39. 49 0
      data/Dockerfiles/solr/solr-schema-7.7.0.xml
  40. 1 1
      data/Dockerfiles/unbound/Dockerfile
  41. 6 2
      data/Dockerfiles/watchdog/Dockerfile
  42. 305 58
      data/Dockerfiles/watchdog/watchdog.sh
  43. 3 3
      data/assets/nextcloud/nextcloud.conf
  44. 1 1
      data/assets/nextcloud/occ
  45. 0 44
      data/assets/nextcloud/site.nextcloud.custom
  46. 31 34
      data/conf/dovecot/dovecot.conf
  47. 3 0
      data/conf/dovecot/global_sieve_after
  48. 2 0
      data/conf/dovecot/global_sieve_before
  49. 14 0
      data/conf/nginx/site.conf
  50. 10 0
      data/conf/nginx/templates/sogo.auth_request.template.sh
  51. 0 0
      data/conf/phpfpm/sogo-sso/.gitkeep
  52. 5 2
      data/conf/postfix/anonymize_headers.pcre
  53. 2 0
      data/conf/postfix/local_transport
  54. 84 26
      data/conf/postfix/main.cf
  55. 19 14
      data/conf/postfix/master.cf
  56. 44 0
      data/conf/rspamd/custom/bad_words.map
  57. 66 0
      data/conf/rspamd/custom/fishy_tlds.map
  58. 4 0
      data/conf/rspamd/custom/ip_wl.map
  59. 35 35
      data/conf/rspamd/dynmaps/settings.php
  60. 2 0
      data/conf/rspamd/local.d/arc.conf
  61. 14 0
      data/conf/rspamd/local.d/composites.conf
  62. 7 0
      data/conf/rspamd/local.d/external_services.conf
  63. 1 1
      data/conf/rspamd/local.d/metadata_exporter.conf
  64. 1 0
      data/conf/rspamd/local.d/milter_headers.conf
  65. 36 0
      data/conf/rspamd/local.d/multimap.conf
  66. 8 2
      data/conf/rspamd/local.d/policies_group.conf
  67. 10 0
      data/conf/rspamd/local.d/rbl.conf
  68. 8 0
      data/conf/rspamd/local.d/rbl_group.conf
  69. 0 16
      data/conf/rspamd/local.d/rspamd.conf.local
  70. 1 0
      data/conf/rspamd/local.d/spamassassin.conf
  71. 2 2
      data/conf/rspamd/local.d/statistics_group.conf
  72. 23 2
      data/conf/rspamd/meta_exporter/pipe.php
  73. 3 3
      data/conf/rspamd/override.d/ratelimit.conf
  74. 0 1
      data/conf/rspamd/override.d/worker-controller-password.inc
  75. 12 0
      data/conf/rspamd/override.d/worker-fuzzy.inc
  76. 1 1
      data/conf/rspamd/override.d/worker-proxy.inc
  77. 1 0
      data/conf/rspamd/plugins.d/README.md
  78. 1 0
      data/conf/rspamd/rspamd.conf.local
  79. 2 0
      data/conf/rspamd/rspamd.conf.override
  80. 0 1
      data/conf/sogo/sogo.conf
  81. 1 0
      data/conf/unbound/unbound.conf
  82. 71 16
      data/web/admin.php
  83. 5 4
      data/web/css/build/001-bootstrap.min.css
  84. 1 1
      data/web/css/build/004-bootstrap-slider.min.css
  85. 3 0
      data/web/css/build/008-mailcow.css
  86. 7 1
      data/web/css/site/admin.css
  87. 5 0
      data/web/css/site/index.css
  88. 7 1
      data/web/css/site/mailbox.css
  89. 17 1
      data/web/css/site/quarantine.css
  90. 8 0
      data/web/css/site/user.css
  91. 71 13
      data/web/edit.php
  92. 1 1
      data/web/inc/ajax/dns_diagnostics.php
  93. 19 1
      data/web/inc/ajax/qitem_details.php
  94. 13 0
      data/web/inc/ajax/qr_gen.php
  95. 5 0
      data/web/inc/ajax/transport_check.php
  96. 13 0
      data/web/inc/footer.inc.php
  97. 1 1
      data/web/inc/functions.address_rewriting.inc.php
  98. 7 2
      data/web/inc/functions.fail2ban.inc.php
  99. 95 5
      data/web/inc/functions.inc.php
  100. 245 60
      data/web/inc/functions.mailbox.inc.php

+ 53 - 18
.github/ISSUE_TEMPLATE/Bug_report.md

@@ -3,28 +3,63 @@ name: Bug report
 about: Report a bug for this project
 about: Report a bug for this project
 
 
 ---
 ---
+<!--
+  For community support and other discussions, you are welcome to visit us on our community channels listed at https://mailcow.github.io/mailcow-dockerized-docs/#community-support. For professional commercial support, please check out https://mailcow.github.io/mailcow-dockerized-docs/#commercial-support instead
+-->
 
 
-**README and remove me**
-For community support and other discussion, you are welcome to visit and stay with us @ Freenode, #mailcow
-Answering can take a few seconds up to many hours, please be patient.
-Commercial support, including a ticket system, can be found @ https://www.servercow.de/mailcow#support - we are also available via Telegram. \o/
+**Prior to placing the issue, please check following:** *(fill out each checkbox with a `X` once done)*
+- [ ] I understand that not following below instructions might result in immediate closing and deletion of my issue.
+- [ ] I have understood that answers are voluntary and community-driven, and not commercial support.
+- [ ] I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues).
 
 
-**Describe the bug, try to make it reproducible**
-A clear and concise description of what the bug is. How can it be reproduced? 
-If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI.
+---
+
+**Description of the bug**: What kind of issue have you *exactly* come across?
+<!--
+  This should be a clear and concise description of what the bug is. What EXACTLY does happen?
+  If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI.
+  Write your detailed description below.
+-->
+
+My issue is...
+
+**Reproduction of said bug**: How *exactly* do you reproduce the bug?
+<!--
+  Here it is really helpful to know how exactly you are able to reproduce the reported issue.
+  Meaning: What are the exact steps - one by one - to get the above described behavior.
+  Screenshots can be added, if helpful. Add the text below.
+-->
 
 
-**System information and quick debugging**
-General logs:
-- Please take a look at the [documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/).
+1. I go to...
+2. And then to...
+3. But once I do...
+
+__I have tried or I do...__ *(fill out each checkbox with a `X` if applicable)*
+- [ ] In case of WebUI issue, I have tried clearing the browser cache and the issue persists.
+- [ ] I do run mailcow on a Synology, QNAP or any other sort of NAS.
+
+**System information**
+<!--
+  In this stage we would kindly ask you to attach logs or general system information about your setup.
+  Please carefully read the questions and instructions below.
+-->
 
 
 Further information (where applicable):
 Further information (where applicable):
- - Your OS (is Apparmor or SELinux active?)
- - Your virtualization technology (KVM/QEMU, Xen, VMware, VirtualBox etc.)
- - Your server/VM specifications (Memory, CPU Cores)
- - Don't try to run mailcow on a Synology or QNAP NAS, do you?
- - Docker and Docker Compose versions
- - Output of `git diff origin/master`, any other changes to the code?
+
+| Question | Answer |
+| --- | --- |
+| My operating system | I_DO_REPLY_HERE |
+| Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE |
+| Virtualization technlogy (KVM, VMware, Xen, etc) | I_DO_REPLY_HERE |
+| Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE |
+| Docker Version (`docker version`) | I_DO_REPLY_HERE |
+| Docker-Compose Version (`docker-compose version`) | I_DO_REPLY_HERE |
+| Reverse proxy (custom solution) | I_DO_REPLY_HERE |
+
+Further notes:
+ - Output of `git diff origin/master`, any other changes to the code? If so, please post them.
  - All third-party firewalls and custom iptables rules are unsupported. Please check the Docker docs about how to use Docker with your own ruleset. Nevertheless, iptabels output can help _us_ to help _you_: `iptables -L -vn`, `ip6tables -L -vn`, `iptables -L -vn -t nat` and `ip6tables -L -vn -t nat `
  - All third-party firewalls and custom iptables rules are unsupported. Please check the Docker docs about how to use Docker with your own ruleset. Nevertheless, iptabels output can help _us_ to help _you_: `iptables -L -vn`, `ip6tables -L -vn`, `iptables -L -vn -t nat` and `ip6tables -L -vn -t nat `
- - Reverse proxy? If you think this problem is related to your reverse proxy, please post your configuration.
- - Browser (if it's a Web UI issue) - please clean your browser cache and try again, problem persists?
  - Check `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @1.1.1.1` - output? Timeout?
  - Check `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @1.1.1.1` - output? Timeout?
+ 
+ General logs:
+- Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/).

+ 6 - 0
.gitignore

@@ -1,10 +1,12 @@
 rebuild-images.sh
 rebuild-images.sh
 data/conf/sogo/sieve.creds
 data/conf/sogo/sieve.creds
+data/conf/phpfpm/sogo-sso/sogo-sso.pass
 data/conf/dovecot/dovecot-master.passwd
 data/conf/dovecot/dovecot-master.passwd
 data/conf/dovecot/dovecot-master.userdb
 data/conf/dovecot/dovecot-master.userdb
 mailcow.conf
 mailcow.conf
 mailcow.conf_backup
 mailcow.conf_backup
 data/conf/nginx/*.active
 data/conf/nginx/*.active
+data/conf/postfix/extra.cf
 data/conf/postfix/sql
 data/conf/postfix/sql
 data/conf/postfix/allow_mailcow_local.regexp
 data/conf/postfix/allow_mailcow_local.regexp
 data/conf/dovecot/sql
 data/conf/dovecot/sql
@@ -24,11 +26,15 @@ data/conf/nginx/*.custom
 data/conf/nginx/*.bak
 data/conf/nginx/*.bak
 data/conf/dovecot/acl_anyone
 data/conf/dovecot/acl_anyone
 data/conf/dovecot/mail_plugins*
 data/conf/dovecot/mail_plugins*
+data/conf/dovecot/sogo-sso.conf
 data/conf/dovecot/extra.conf
 data/conf/dovecot/extra.conf
+data/conf/dovecot/shared_namespace.conf
 data/conf/rspamd/custom/*
 data/conf/rspamd/custom/*
 data/conf/portainer/
 data/conf/portainer/
 data/gitea/
 data/gitea/
 data/gogs/
 data/gogs/
 data/conf/sogo/plist_ldap
 data/conf/sogo/plist_ldap
+update_diffs/
 .github/
 .github/
 docker-compose.override.yml
 docker-compose.override.yml
+refresh_images.sh

+ 1 - 1
README.md

@@ -2,7 +2,7 @@
 
 
 ## Want to support mailcow?
 ## Want to support mailcow?
 
 
-Donate via **PayPal** [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) or via **Liberapay** [![Liberapay.com](https://mailcow.email/img/lp.png)](https://liberapay.com/mailcow)
+Please [consider a support contract (around 30 € per month) with Servercow](https://www.servercow.de/mailcow#support) to support further development. _We_ support _you_ while _you_ support _us_. :)
 
 
 Or just spread the word: moo.
 Or just spread the word: moo.
 
 

+ 6 - 5
data/Dockerfiles/acme/Dockerfile

@@ -1,8 +1,9 @@
-FROM alpine:3.9
+FROM alpine:3.10
 
 
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
-RUN apk add --update --no-cache \
+RUN apk upgrade --no-cache \
+  && apk add --update --no-cache \
   bash \
   bash \
   curl \
   curl \
   openssl \
   openssl \
@@ -12,9 +13,9 @@ RUN apk add --update --no-cache \
   redis \
   redis \
   tini \
   tini \
   tzdata \
   tzdata \
-  py-pip \
-  && pip install --upgrade pip \
-  && pip install acme-tiny
+  python3 \
+  && python3 -m pip install --upgrade pip \
+  && python3 -m pip install acme-tiny
 
 
 COPY docker-entrypoint.sh /srv/docker-entrypoint.sh
 COPY docker-entrypoint.sh /srv/docker-entrypoint.sh
 COPY expand6.sh /srv/expand6.sh
 COPY expand6.sh /srv/expand6.sh

+ 54 - 31
data/Dockerfiles/acme/docker-entrypoint.sh

@@ -5,6 +5,21 @@ exec 5>&1
 # Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
 # Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
 source /srv/expand6.sh
 source /srv/expand6.sh
 
 
+# Skipping IP check when we like to live dangerously
+if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  SKIP_IP_CHECK=y
+fi
+
+# Skipping HTTP check when we like to live dangerously
+if [[ "${SKIP_HTTP_VERIFICATION}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  SKIP_HTTP_VERIFICATION=y
+fi
+
+# Request certificate for MAILCOW_HOSTNAME ony
+if [[ "${ONLY_MAILCOW_HOSTNAME}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  ONLY_MAILCOW_HOSTNAME=y
+fi
+
 log_f() {
 log_f() {
   if [[ ${2} == "no_nl" ]]; then
   if [[ ${2} == "no_nl" ]]; then
     echo -n "$(date) - ${1}"
     echo -n "$(date) - ${1}"
@@ -42,7 +57,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.
@@ -118,21 +132,25 @@ get_ipv6(){
 }
 }
 
 
 verify_challenge_path(){
 verify_challenge_path(){
+  if [[ ${SKIP_HTTP_VERIFICATION} == "y" ]]; then
+    echo '(skipping check, returning 0)'
+    return 0
+  fi
   # verify_challenge_path URL 4|6
   # verify_challenge_path URL 4|6
-  RAND_FILE=${RANDOM}${RANDOM}${RANDOM}
-  touch /var/www/acme/${RAND_FILE}
-  if [[ "$(curl -${2} http://${1}/.well-known/acme-challenge/${RAND_FILE} --write-out %{http_code} --silent --output /dev/null)" =~ ^(2|3)  ]]; then
-    rm /var/www/acme/${RAND_FILE}
+  RANDOM_N=${RANDOM}${RANDOM}${RANDOM}
+  echo ${RANDOM_N} > /var/www/acme/${RANDOM_N}
+  if [[ "$(curl --insecure -${2} -L http://${1}/.well-known/acme-challenge/${RANDOM_N} --silent)" == "${RANDOM_N}"  ]]; then
+    rm /var/www/acme/${RANDOM_N}
     return 0
     return 0
   else
   else
-    rm /var/www/acme/${RAND_FILE}
+    rm /var/www/acme/${RANDOM_N}
     return 1
     return 1
   fi
   fi
 }
 }
 
 
 [[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem
 [[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem
 
 
-if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; then
+if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]] && [[ $(stat -c%s ${ACME_BASE}/cert.pem) != 0 ]]; then
   ISSUER=$(openssl x509 -in ${ACME_BASE}/cert.pem -noout -issuer)
   ISSUER=$(openssl x509 -in ${ACME_BASE}/cert.pem -noout -issuer)
   if [[ ${ISSUER} != *"Let's Encrypt"* && ${ISSUER} != *"mailcow"* && ${ISSUER} != *"Fake LE Intermediate"* ]]; then
   if [[ ${ISSUER} != *"Let's Encrypt"* && ${ISSUER} != *"mailcow"* && ${ISSUER} != *"Fake LE Intermediate"* ]]; then
     log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..."
     log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..."
@@ -156,6 +174,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,10 +215,8 @@ 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
 
 
-  # Skipping IP check when we like to live dangerously
-  if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-    SKIP_IP_CHECK=y
-  fi
+  chmod 600 ${ACME_BASE}/acme/key.pem
+  chmod 600 ${ACME_BASE}/acme/account.pem
 
 
   # Cleaning up and init validation arrays
   # Cleaning up and init validation arrays
   unset SQL_DOMAIN_ARR
   unset SQL_DOMAIN_ARR
@@ -228,7 +245,7 @@ while true; do
       ADDITIONAL_SAN_ARR+=($i)
       ADDITIONAL_SAN_ARR+=($i)
     fi
     fi
   done
   done
-  ADDITIONAL_WC_ARR+=('autodiscover')
+  ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
 
 
   # Start IP detection
   # Start IP detection
   log_f "Detecting IP addresses... " no_nl
   log_f "Detecting IP addresses... " no_nl
@@ -255,9 +272,10 @@ while true; do
     SQL_DOMAIN_ARR+=("${domains}")
     SQL_DOMAIN_ARR+=("${domains}")
   done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs)
   done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs)
 
 
+  if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
   for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
   for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
     for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
     for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
-      if [[  "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
+      if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
         A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
         A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
         AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
         AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
         # Check if CNAME without v6 enabled target
         # Check if CNAME without v6 enabled target
@@ -268,10 +286,10 @@ while true; do
           log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check"
           log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check"
           if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
           if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
             if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 6; then
             if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 6; then
-              log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}"
+              log_f "Confirmed AAAA record with IP ${AAAA_SUBDOMAIN}, adding SAN"
               VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
               VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
             else
             else
-              log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}, but HTTP validation failed"
+              log_f "Confirmed AAAA record with IP ${AAAA_SUBDOMAIN}, but HTTP validation failed"
             fi
             fi
           else
           else
             log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))"
             log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))"
@@ -280,10 +298,10 @@ while true; do
           log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}"
           log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}"
           if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
           if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
             if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 4; then
             if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 4; then
-              log_f "Confirmed A record ${A_SUBDOMAIN}"
+              log_f "Confirmed A record ${A_SUBDOMAIN}, adding SAN"
               VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
               VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
             else
             else
-              log_f "Confirmed AAAA record ${A_SUBDOMAIN}, but HTTP validation failed"
+              log_f "Confirmed A record with IP ${A_SUBDOMAIN}, but HTTP validation failed"
             fi
             fi
           else
           else
             log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})"
             log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})"
@@ -294,6 +312,7 @@ while true; do
       fi
       fi
     done
     done
   done
   done
+  fi
 
 
   A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1)
   A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1)
   AAAA_MAILCOW_HOSTNAME=$(dig AAAA ${MAILCOW_HOSTNAME} +short | tail -n 1)
   AAAA_MAILCOW_HOSTNAME=$(dig AAAA ${MAILCOW_HOSTNAME} +short | tail -n 1)
@@ -308,10 +327,10 @@ while true; do
         log_f "Confirmed AAAA record ${AAAA_MAILCOW_HOSTNAME}"
         log_f "Confirmed AAAA record ${AAAA_MAILCOW_HOSTNAME}"
         VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
         VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
       else
       else
-        log_f "Confirmed AAAA record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
+        log_f "Confirmed AAAA record with IP ${AAAA_MAILCOW_HOSTNAME}, but HTTP validation failed"
       fi
       fi
     else
     else
-      log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} ($(expand ${AAAA_MAILCOW_HOSTNAME}))"
+      log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} (DNS returned $(expand ${AAAA_MAILCOW_HOSTNAME}))"
     fi
     fi
   elif [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
   elif [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
     log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
     log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
@@ -320,15 +339,16 @@ while true; do
         log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}"
         log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}"
         VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
         VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
       else
       else
-        log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
+        log_f "Confirmed A record with IP ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
       fi
       fi
     else
     else
-      log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME})"
+      log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (DNS returned ${A_MAILCOW_HOSTNAME})"
     fi
     fi
   else
   else
     log_f "No A or AAAA record found for hostname ${MAILCOW_HOSTNAME}"
     log_f "No A or AAAA record found for hostname ${MAILCOW_HOSTNAME}"
   fi
   fi
 
 
+  if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
   for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do
   for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do
     # Skip on CAA errors for SAN
     # Skip on CAA errors for SAN
     SAN_PARENT_DOMAIN=$(echo ${SAN} | cut -d. -f2-)
     SAN_PARENT_DOMAIN=$(echo ${SAN} | cut -d. -f2-)
@@ -354,13 +374,13 @@ while true; do
       log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check"
       log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check"
       if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
       if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
         if verify_challenge_path "${SAN}" 6; then
         if verify_challenge_path "${SAN}" 6; then
-          log_f "Confirmed AAAA record ${AAAA_SAN}"
+          log_f "Confirmed AAAA record with IP ${AAAA_SAN}"
           ADDITIONAL_VALIDATED_SAN+=("${SAN}")
           ADDITIONAL_VALIDATED_SAN+=("${SAN}")
         else
         else
-          log_f "Confirmed AAAA record ${AAAA_SAN}, but HTTP validation failed"
+          log_f "Confirmed AAAA record with IP ${AAAA_SAN}, but HTTP validation failed"
         fi
         fi
       else
       else
-        log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} ($(expand ${AAAA_SAN}))"
+        log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} (DNS returned $(expand ${AAAA_SAN}))"
       fi
       fi
     elif [[ ! -z ${A_SAN} ]]; then
     elif [[ ! -z ${A_SAN} ]]; then
       log_f "Found A record for ${SAN}: ${A_SAN}"
       log_f "Found A record for ${SAN}: ${A_SAN}"
@@ -369,21 +389,23 @@ while true; do
           log_f "Confirmed A record ${A_SAN}"
           log_f "Confirmed A record ${A_SAN}"
           ADDITIONAL_VALIDATED_SAN+=("${SAN}")
           ADDITIONAL_VALIDATED_SAN+=("${SAN}")
         else
         else
-          log_f "Confirmed A record ${A_SAN}, but HTTP validation failed"
+          log_f "Confirmed A record with IP ${A_SAN}, but HTTP validation failed"
         fi
         fi
       else
       else
-        log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (${A_SAN})"
+        log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (DNS returned ${A_SAN})"
       fi
       fi
     else
     else
       log_f "No A or AAAA record found for hostname ${SAN}"
       log_f "No A or AAAA record found for hostname ${SAN}"
     fi
     fi
   done
   done
+  fi
 
 
   # Unique elements
   # Unique elements
   ALL_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
   ALL_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
   if [[ -z ${ALL_VALIDATED[*]} ]]; then
   if [[ -z ${ALL_VALIDATED[*]} ]]; then
     log_f "Cannot validate hostnames, skipping Let's Encrypt for 1 hour."
     log_f "Cannot validate hostnames, skipping Let's Encrypt for 1 hour."
     log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently."
     log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently."
+    redis-cli -h redis SET ACME_FAIL_TIME "$(date +%s)"
     sleep 1h
     sleep 1h
     exec $(readlink -f "$0")
     exec $(readlink -f "$0")
   fi
   fi
@@ -397,19 +419,19 @@ while true; do
   # Finding difference in SAN array now vs. SAN array by current configuration
   # Finding difference in SAN array now vs. SAN array by current configuration
   array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED
   array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED
   if [[ ! -z ${ORPHANED_SAN[*]} ]]; then
   if [[ ! -z ${ORPHANED_SAN[*]} ]]; then
-    log_f "Found orphaned SANs ${ORPHANED_SAN[*]}"
+    log_f "Found orphaned SAN ${ORPHANED_SAN[*]}"
     SAN_CHANGE=1
     SAN_CHANGE=1
   fi
   fi
   array_diff ADDED_SAN ALL_VALIDATED SAN_ARRAY_NOW
   array_diff ADDED_SAN ALL_VALIDATED SAN_ARRAY_NOW
   if [[ ! -z ${ADDED_SAN[*]} ]]; then
   if [[ ! -z ${ADDED_SAN[*]} ]]; then
-    log_f "Found new SANs ${ADDED_SAN[*]}"
+    log_f "Found new SAN ${ADDED_SAN[*]}"
     SAN_CHANGE=1
     SAN_CHANGE=1
   fi
   fi
 
 
   if [[ ${SAN_CHANGE} == 0 ]]; then
   if [[ ${SAN_CHANGE} == 0 ]]; then
     # Certificate did not change but could be due for renewal (4 weeks)
     # Certificate did not change but could be due for renewal (4 weeks)
-    if ! openssl x509 -checkend 1209600 -noout -in ${ACME_BASE}/cert.pem; then
-      log_f "Certificate is due for renewal (< 2 weeks)"
+    if ! openssl x509 -checkend 2592000 -noout -in ${ACME_BASE}/cert.pem; then
+      log_f "Certificate is due for renewal (< 30 days)"
     else
     else
       log_f "Certificate validation done, neither changed nor due for renewal, sleeping for another day."
       log_f "Certificate validation done, neither changed nor due for renewal, sleeping for another day."
       sleep 1d
       sleep 1d
@@ -462,7 +484,7 @@ while true; do
         cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem
         cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem
         cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem
         cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem
         reload_configurations
         reload_configurations
-        rm /var/www/acme/*
+        rm /var/www/acme/* 2> /dev/null
         log_f "Certificate successfully deployed, removing backup, sleeping 1d"
         log_f "Certificate successfully deployed, removing backup, sleeping 1d"
         sleep 1d
         sleep 1d
       else
       else
@@ -476,6 +498,7 @@ while true; do
       ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
       ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
       log_f "${ACME_RESPONSE_B64}" redis_only b64
       log_f "${ACME_RESPONSE_B64}" redis_only b64
       log_f "Retrying in 30 minutes..."
       log_f "Retrying in 30 minutes..."
+      redis-cli -h redis SET ACME_FAIL_TIME "$(date +%s)"
       sleep 30m
       sleep 30m
       exec $(readlink -f "$0")
       exec $(readlink -f "$0")
       ;;
       ;;

+ 1 - 1
data/Dockerfiles/clamd/Dockerfile

@@ -3,7 +3,7 @@ FROM debian:stretch-slim
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 
 
 # Installation
 # Installation
-ENV CLAMAV 0.101.1
+ENV CLAMAV 0.101.4
 
 
 RUN apt-get update && apt-get install -y --no-install-recommends \
 RUN apt-get update && apt-get install -y --no-install-recommends \
   ca-certificates \
   ca-certificates \

+ 4 - 1
data/Dockerfiles/clamd/bootstrap.sh

@@ -48,6 +48,7 @@ while true; do
   sleep 2m
   sleep 2m
   SANE_MIRRORS="$(dig +ignore +short rsync.sanesecurity.net)"
   SANE_MIRRORS="$(dig +ignore +short rsync.sanesecurity.net)"
   for sane_mirror in ${SANE_MIRRORS}; do
   for sane_mirror in ${SANE_MIRRORS}; do
+    CE=
     rsync -avp --chown=clamav:clamav --chmod=Du=rwx,Dgo=rx,Fu=rw,Fog=r --timeout=5 rsync://${sane_mirror}/sanesecurity/ \
     rsync -avp --chown=clamav:clamav --chmod=Du=rwx,Dgo=rx,Fu=rw,Fog=r --timeout=5 rsync://${sane_mirror}/sanesecurity/ \
       --include 'blurl.ndb' \
       --include 'blurl.ndb' \
       --include 'junk.ndb' \
       --include 'junk.ndb' \
@@ -61,7 +62,9 @@ while true; do
       --include 'sanesecurity.ftm' \
       --include 'sanesecurity.ftm' \
       --include 'sigwhitelist.ign2' \
       --include 'sigwhitelist.ign2' \
       --exclude='*' /var/lib/clamav/
       --exclude='*' /var/lib/clamav/
-    if [ $? -eq 0 ]; then
+    CE=$?
+    chmod 755 /var/lib/clamav/
+    if [ ${CE} -eq 0 ]; then
       echo RELOAD | nc localhost 3310
       echo RELOAD | nc localhost 3310
       break
       break
     fi
     fi

+ 8 - 7
data/Dockerfiles/dockerapi/Dockerfile

@@ -1,11 +1,12 @@
-FROM alpine:3.9
+FROM alpine:3.10
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
-RUN apk add -U --no-cache python2 python-dev py-pip gcc musl-dev tzdata openssl-dev libffi-dev \
-  && pip2 install --upgrade pip \
-  && pip2 install --upgrade docker==3.0.1 flask flask-restful pyOpenSSL \
-  && apk del python-dev py2-pip gcc
+WORKDIR /app
 
 
-COPY server.py /
+RUN apk add --update --no-cache python3 openssl tzdata \
+ && pip3 install --upgrade pip \
+ && pip3 install --upgrade docker flask flask-restful
 
 
-CMD ["python2", "-u", "/server.py"]
+COPY server.py /app/
+
+CMD ["python3", "-u", "/app/server.py"]

+ 314 - 315
data/Dockerfiles/dockerapi/server.py

@@ -1,10 +1,11 @@
+#!/usr/bin/env python3
+
 from flask import Flask
 from flask import Flask
 from flask_restful import Resource, Api
 from flask_restful import Resource, Api
 from flask import jsonify
 from flask import jsonify
 from flask import Response
 from flask import Response
 from flask import request
 from flask import request
 from threading import Thread
 from threading import Thread
-from OpenSSL import crypto
 import docker
 import docker
 import uuid
 import uuid
 import signal
 import signal
@@ -14,6 +15,8 @@ import re
 import sys
 import sys
 import ssl
 import ssl
 import socket
 import socket
+import subprocess
+import traceback
 
 
 docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
 docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
 app = Flask(__name__)
 app = Flask(__name__)
@@ -43,261 +46,316 @@ class container_get(Resource):
 class container_post(Resource):
 class container_post(Resource):
   def post(self, container_id, post_action):
   def post(self, container_id, post_action):
     if container_id and container_id.isalnum() and post_action:
     if container_id and container_id.isalnum() and post_action:
-      if post_action == 'stop':
-        try:
-          for container in docker_client.containers.list(all=True, filters={"id": container_id}):
-            container.stop()
-          return jsonify(type='success', msg='command completed successfully')
-        except Exception as e:
-          return jsonify(type='danger', msg=str(e))
+      try:
+        """Dispatch container_post api call"""
+        if post_action == 'exec':
+          if not request.json or not 'cmd' in request.json:
+            return jsonify(type='danger', msg='cmd is missing')
+          if not request.json or not 'task' in request.json:
+            return jsonify(type='danger', msg='task is missing')
 
 
-      elif post_action == 'start':
-        try:
-          for container in docker_client.containers.list(all=True, filters={"id": container_id}):
-            container.start()
-          return jsonify(type='success', msg='command completed successfully')
-        except Exception as e:
-          return jsonify(type='danger', msg=str(e))
+          api_call_method_name = '__'.join(['container_post', str(post_action), str(request.json['cmd']), str(request.json['task']) ])
+        else:
+          api_call_method_name = '__'.join(['container_post', str(post_action) ])
 
 
-      elif post_action == 'restart':
-        try:
-          for container in docker_client.containers.list(all=True, filters={"id": container_id}):
-            container.restart()
-          return jsonify(type='success', msg='command completed successfully')
-        except Exception as e:
-          return jsonify(type='danger', msg=str(e))
+        api_call_method = getattr(self, api_call_method_name, lambda container_id: jsonify(type='danger', msg='container_post - unknown api call'))
 
 
-      elif post_action == 'top':
-        try:
-          for container in docker_client.containers.list(all=True, filters={"id": container_id}):
-            return jsonify(type='success', msg=container.top())
-        except Exception as e:
-          return jsonify(type='danger', msg=str(e))
 
 
-      elif post_action == 'stats':
-        try:
-          for container in docker_client.containers.list(all=True, filters={"id": container_id}):
-            return jsonify(type='success', msg=container.stats(decode=True, stream=False))
-        except Exception as e:
-          return jsonify(type='danger', msg=str(e))
+        print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
+        return api_call_method(container_id)
+      except Exception as e:
+        print("error - container_post: %s" % str(e))  
+        return jsonify(type='danger', msg=str(e))
+
+    else:
+      return jsonify(type='danger', msg='invalid container id or missing action')        
+
+
+  # api call: container_post - post_action: stop
+  def container_post__stop(self, container_id):
+    for container in docker_client.containers.list(all=True, filters={"id": container_id}):
+      container.stop()
+    return jsonify(type='success', msg='command completed successfully')
+
+
+  # api call: container_post - post_action: start
+  def container_post__start(self, container_id):
+    for container in docker_client.containers.list(all=True, filters={"id": container_id}):
+      container.start()
+    return jsonify(type='success', msg='command completed successfully')
+
+
+  # api call: container_post - post_action: restart
+  def container_post__restart(self, container_id):
+    for container in docker_client.containers.list(all=True, filters={"id": container_id}):
+      container.restart()
+    return jsonify(type='success', msg='command completed successfully')
+
+
+  # api call: container_post - post_action: top
+  def container_post__top(self, container_id):
+    for container in docker_client.containers.list(all=True, filters={"id": container_id}):
+      return jsonify(type='success', msg=container.top())
+
+
+  # api call: container_post - post_action: stats
+  def container_post__stats(self, container_id):
+    for container in docker_client.containers.list(all=True, filters={"id": container_id}):
+      for stat in container.stats(decode=True, stream=True):
+        return jsonify(type='success', msg=stat )
+
+
+  # api call: container_post - post_action: exec - cmd: mailq - task: delete
+  def container_post__exec__mailq__delete(self, container_id):
+    if 'items' in request.json:  
+      r = re.compile("^[0-9a-fA-F]+$")
+      filtered_qids = filter(r.match, request.json['items'])
+      if filtered_qids:
+        flagged_qids = ['-d %s' % i for i in filtered_qids]
+        sanitized_string = str(' '.join(flagged_qids));
+
+        for container in docker_client.containers.list(filters={"id": container_id}):
+          postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
+          return exec_run_handler('generic', postsuper_r)
+
+
+  # api call: container_post - post_action: exec - cmd: mailq - task: hold
+  def container_post__exec__mailq__hold(self, container_id):
+    if 'items' in request.json:  
+      r = re.compile("^[0-9a-fA-F]+$")
+      filtered_qids = filter(r.match, request.json['items'])
+      if filtered_qids:
+        flagged_qids = ['-h %s' % i for i in filtered_qids]
+        sanitized_string = str(' '.join(flagged_qids));
 
 
-      elif post_action == 'exec':
-
-        if not request.json or not 'cmd' in request.json:
-          return jsonify(type='danger', msg='cmd is missing')
-
-        if request.json['cmd'] == 'mailq':
-          if 'items' in request.json:
-            r = re.compile("^[0-9a-fA-F]+$")
-            filtered_qids = filter(r.match, request.json['items'])
-            if filtered_qids:
-              if request.json['task'] == 'delete':
-                flagged_qids = ['-d %s' % i for i in filtered_qids]
-                sanitized_string = str(' '.join(flagged_qids));
-                try:
-                  for container in docker_client.containers.list(filters={"id": container_id}):
-                    postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
-                    return exec_run_handler('generic', postsuper_r)
-                except Exception as e:
-                  return jsonify(type='danger', msg=str(e))
-              if request.json['task'] == 'hold':
-                flagged_qids = ['-h %s' % i for i in filtered_qids]
-                sanitized_string = str(' '.join(flagged_qids));
-                try:
-                  for container in docker_client.containers.list(filters={"id": container_id}):
-                    postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
-                    return exec_run_handler('generic', postsuper_r)
-                except Exception as e:
-                  return jsonify(type='danger', msg=str(e))
-              if request.json['task'] == 'unhold':
-                flagged_qids = ['-H %s' % i for i in filtered_qids]
-                sanitized_string = str(' '.join(flagged_qids));
-                try:
-                  for container in docker_client.containers.list(filters={"id": container_id}):
-                    postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
-                    return exec_run_handler('generic', postsuper_r)
-                except Exception as e:
-                  return jsonify(type='danger', msg=str(e))
-              if request.json['task'] == 'deliver':
-                flagged_qids = ['-i %s' % i for i in filtered_qids]
-                try:
-                  for container in docker_client.containers.list(filters={"id": container_id}):
-                    for i in flagged_qids:
-                      postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
-                      # todo: check each exit code
-                    return jsonify(type='success', msg=str("Scheduled immediate delivery"))
-                except Exception as e:
-                  return jsonify(type='danger', msg=str(e))
-          elif request.json['task'] == 'list':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
-                return exec_run_handler('utf8_text_only', mailq_return)
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-          elif request.json['task'] == 'flush':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
-                return exec_run_handler('generic', postqueue_r)
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-          elif request.json['task'] == 'super_delete':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
-                return exec_run_handler('generic', postsuper_r)
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-
-        elif request.json['cmd'] == 'system':
-          if request.json['task'] == 'fts_rescan':
-            if 'username' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail')
-                  if rescan_return.exit_code == 0:
-                    return jsonify(type='success', msg='fts_rescan: rescan triggered')
-                  else:
-                    return jsonify(type='warning', msg='fts_rescan error')
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
-            if 'all' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -A"], user='vmail')
-                  if rescan_return.exit_code == 0:
-                    return jsonify(type='success', msg='fts_rescan: rescan triggered')
-                  else:
-                    return jsonify(type='warning', msg='fts_rescan error')
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
-          elif request.json['task'] == 'df':
-            if 'dir' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
-                  if df_return.exit_code == 0:
-                    return df_return.output.rstrip()
-                  else:
-                    return "0,0,0,0,0,0"
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
-          elif request.json['task'] == 'mysql_upgrade':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                sql_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='mysql')
-                upgrade_cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"
-                sql_socket = sql_shell.output;
-                try :
-                  sql_socket.sendall(upgrade_cmd.encode('utf-8'))
-                  sql_socket.shutdown(socket.SHUT_WR)
-                except socket.error:
-                  return jsonify(type='danger', msg=str('socket error'))
-                worker_response = recv_socket_data(sql_socket)
-                matched = False
-                for line in worker_response.split("\n"):
-                  if 'is already upgraded to' in line:
-                    matched = True
-                if matched:
-                  return jsonify(type='success', msg='mysql_upgrade: already upgraded')
-                else:
-                  container.restart()
-                  return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied')
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-
-        elif request.json['cmd'] == 'reload':
-          if request.json['task'] == 'dovecot':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                reload_return = container.exec_run(["/bin/bash", "-c", "/usr/local/sbin/dovecot reload"])
-                return exec_run_handler('generic', reload_return)
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-          if request.json['task'] == 'postfix':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
-                return exec_run_handler('generic', reload_return)
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-          if request.json['task'] == 'nginx':
-            try:
-              for container in docker_client.containers.list(filters={"id": container_id}):
-                reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
-                return exec_run_handler('generic', reload_return)
-            except Exception as e:
-              return jsonify(type='danger', msg=str(e))
-
-        elif request.json['cmd'] == 'sieve':
-          if request.json['task'] == 'list':
-            if 'username' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"])
-                  return exec_run_handler('utf8_text_only', sieve_return)
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
-          elif request.json['task'] == 'print':
-            if 'username' in request.json and 'script_name' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"])
-                  return exec_run_handler('utf8_text_only', sieve_return)
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
-
-        elif request.json['cmd'] == 'maildir':
-          if request.json['task'] == 'cleanup':
-            if 'maildir' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  sane_name = re.sub(r'\W+', '', request.json['maildir'])
-                  maildir_cleanup = container.exec_run(["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"], user='vmail')
-                  return exec_run_handler('generic', maildir_cleanup)
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
-
-        elif request.json['cmd'] == 'rspamd':
-          if request.json['task'] == 'worker_password':
-            if 'raw' in request.json:
-              try:
-                for container in docker_client.containers.list(filters={"id": container_id}):
-                  worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd')
-                  worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n"
-                  worker_socket = worker_shell.output;
-                  try :
-                    worker_socket.sendall(worker_cmd.encode('utf-8'))
-                    worker_socket.shutdown(socket.SHUT_WR)
-                  except socket.error:
-                    return jsonify(type='danger', msg=str('socket error'))
-                  worker_response = recv_socket_data(worker_socket)
-                  matched = False
-                  for line in worker_response.split("\n"):
-                    if '$2$' in line:
-                      matched = True
-                      hash = line.strip()
-                      hash_out = re.search('\$2\$.+$', hash).group(0)
-                      f = open("/access.inc", "w")
-                      f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n')
-                      f.close()
-                      container.restart()
-                  if matched:
-                    return jsonify(type='success', msg='command completed successfully')
-                  else:
-                    return jsonify(type='danger', msg='command did not complete')
-              except Exception as e:
-                return jsonify(type='danger', msg=str(e))
+        for container in docker_client.containers.list(filters={"id": container_id}):
+          postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
+          return exec_run_handler('generic', postsuper_r)
 
 
+
+   # api call: container_post - post_action: exec - cmd: mailq - task: unhold
+  def container_post__exec__mailq__unhold(self, container_id):
+    if 'items' in request.json:  
+      r = re.compile("^[0-9a-fA-F]+$")
+      filtered_qids = filter(r.match, request.json['items'])
+      if filtered_qids:
+        flagged_qids = ['-H %s' % i for i in filtered_qids]
+        sanitized_string = str(' '.join(flagged_qids));
+
+        for container in docker_client.containers.list(filters={"id": container_id}):
+          postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
+          return exec_run_handler('generic', postsuper_r)
+
+
+  # api call: container_post - post_action: exec - cmd: mailq - task: deliver
+  def container_post__exec__mailq__deliver(self, container_id):
+    if 'items' in request.json:  
+      r = re.compile("^[0-9a-fA-F]+$")
+      filtered_qids = filter(r.match, request.json['items'])
+      if filtered_qids:
+        flagged_qids = ['-i %s' % i for i in filtered_qids]
+
+        for container in docker_client.containers.list(filters={"id": container_id}):
+          for i in flagged_qids:
+            postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
+            # todo: check each exit code
+          return jsonify(type='success', msg=str("Scheduled immediate delivery"))
+
+
+  # api call: container_post - post_action: exec - cmd: mailq - task: list
+  def container_post__exec__mailq__list(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
+      return exec_run_handler('utf8_text_only', mailq_return)
+
+
+  # api call: container_post - post_action: exec - cmd: mailq - task: flush
+  def container_post__exec__mailq__flush(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
+      return exec_run_handler('generic', postqueue_r)
+
+
+  # api call: container_post - post_action: exec - cmd: mailq - task: super_delete
+  def container_post__exec__mailq__super_delete(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
+      return exec_run_handler('generic', postsuper_r)
+
+
+  # api call: container_post - post_action: exec - cmd: system - task: fts_rescan
+  def container_post__exec__system__fts_rescan(self, container_id):
+    if 'username' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail')
+        if rescan_return.exit_code == 0:
+          return jsonify(type='success', msg='fts_rescan: rescan triggered')
         else:
         else:
-          return jsonify(type='danger', msg='Unknown command')
+          return jsonify(type='warning', msg='fts_rescan error')
 
 
+    if 'all' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
+        if rescan_return.exit_code == 0:
+          return jsonify(type='success', msg='fts_rescan: rescan triggered')
+        else:
+          return jsonify(type='warning', msg='fts_rescan error')
+
+
+  # api call: container_post - post_action: exec - cmd: system - task: df
+  def container_post__exec__system__df(self, container_id):
+    if 'dir' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
+        if df_return.exit_code == 0:
+          return df_return.output.decode('utf-8').rstrip()
+        else:
+          return "0,0,0,0,0,0"
+
+
+  # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
+  def container_post__exec__system__mysql_upgrade(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"
+      cmd_response = exec_cmd_container(container, cmd, user='mysql')
+
+      matched = False
+      for line in cmd_response.split("\n"):
+        if 'is already upgraded to' in line:
+          matched = True
+      if matched:
+        return jsonify(type='success', msg='mysql_upgrade: already upgraded')
       else:
       else:
-        return jsonify(type='danger', msg='invalid action')
+        container.restart()
+        return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied')
+
+
+  # api call: container_post - post_action: exec - cmd: reload - task: dovecot
+  def container_post__exec__reload__dovecot(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
+      return exec_run_handler('generic', reload_return)
+
+
+  # api call: container_post - post_action: exec - cmd: reload - task: postfix
+  def container_post__exec__reload__postfix(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
+      return exec_run_handler('generic', reload_return)
+
+
+  # api call: container_post - post_action: exec - cmd: reload - task: nginx
+  def container_post__exec__reload__nginx(self, container_id):
+    for container in docker_client.containers.list(filters={"id": container_id}):
+      reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
+      return exec_run_handler('generic', reload_return)
 
 
+
+  # api call: container_post - post_action: exec - cmd: sieve - task: list
+  def container_post__exec__sieve__list(self, container_id):
+    if 'username' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"])
+        return exec_run_handler('utf8_text_only', sieve_return)
+
+
+  # api call: container_post - post_action: exec - cmd: sieve - task: print
+  def container_post__exec__sieve__print(self, container_id):
+    if 'username' in request.json and 'script_name' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"]  
+        sieve_return = container.exec_run(cmd)
+        return exec_run_handler('utf8_text_only', sieve_return)
+
+
+  # api call: container_post - post_action: exec - cmd: maildir - task: cleanup
+  def container_post__exec__maildir__cleanup(self, container_id):
+    if 'maildir' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        sane_name = re.sub(r'\W+', '', request.json['maildir'])
+        cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
+        maildir_cleanup = container.exec_run(cmd, user='vmail')
+        return exec_run_handler('generic', maildir_cleanup)
+
+
+
+  # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
+  def container_post__exec__rspamd__worker_password(self, container_id):
+    if 'raw' in request.json:
+      for container in docker_client.containers.list(filters={"id": container_id}):
+        cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
+        cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
+        
+        matched = False
+        for line in cmd_response.split("\n"):
+          if '$2$' in line:
+            hash = line.strip()
+            hash_out = re.search('\$2\$.+$', hash).group(0)
+            rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
+
+            rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
+            cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
+            cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
+
+            if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
+              container.restart()
+              matched = True
+
+        if matched:
+            return jsonify(type='success', msg='command completed successfully')
+        else:
+            return jsonify(type='danger', msg='command did not complete')
+        
+
+def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
+
+  def recv_socket_data(c_socket, timeout):
+    c_socket.setblocking(0)
+    total_data=[];
+    data='';
+    begin=time.time()
+    while True:
+      if total_data and time.time()-begin > timeout:
+        break
+      elif time.time()-begin > timeout*2:
+        break
+      try:
+        data = c_socket.recv(8192)
+        if data:
+          total_data.append(data.decode('utf-8'))
+          #change the beginning time for measurement
+          begin=time.time()
+        else:
+          #sleep for sometime to indicate a gap
+          time.sleep(0.1)
+          break
+      except:
+        pass
+    return ''.join(total_data)
+    
+  try :
+    socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
+    if not cmd.endswith("\n"):
+      cmd = cmd + "\n"
+    socket.send(cmd.encode('utf-8'))
+    data = recv_socket_data(socket, timeout)
+    socket.close()
+    return data
+
+  except Exception as e:
+    print("error - exec_cmd_container: %s" % str(e))
+    traceback.print_exc(file=sys.stdout)
+
+def exec_run_handler(type, output):
+  if type == 'generic':
+    if output.exit_code == 0:
+      return jsonify(type='success', msg='command completed successfully')
     else:
     else:
-      return jsonify(type='danger', msg='invalid container id or missing action')
+      return jsonify(type='danger', msg='command failed: ' + output.output.decode('utf-8'))
+  if type == 'utf8_text_only':
+    r = Response(response=output.output.decode('utf-8'), status=200, mimetype="text/plain")
+    r.headers["Content-Type"] = "text/plain; charset=utf-8"
+    return r
 
 
 class GracefulKiller:
 class GracefulKiller:
   kill_now = False
   kill_now = False
@@ -308,84 +366,26 @@ class GracefulKiller:
   def exit_gracefully(self, signum, frame):
   def exit_gracefully(self, signum, frame):
     self.kill_now = True
     self.kill_now = True
 
 
+def create_self_signed_cert():
+    process = subprocess.Popen(
+      "openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /app/dockerapi_key.pem -out /app/dockerapi_cert.pem -subj /CN=dockerapi/O=mailcow -addext subjectAltName=DNS:dockerapi".split(),
+      stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=False
+    )
+    process.wait()
+
 def startFlaskAPI():
 def startFlaskAPI():
   create_self_signed_cert()
   create_self_signed_cert()
   try:
   try:
     ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
     ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
     ctx.check_hostname = False
     ctx.check_hostname = False
-    ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem')
+    ctx.load_cert_chain(certfile='/app/dockerapi_cert.pem', keyfile='/app/dockerapi_key.pem')
   except:
   except:
-    print "Cannot initialize TLS, retrying in 5s..."
+    print ("Cannot initialize TLS, retrying in 5s...")
     time.sleep(5)
     time.sleep(5)
   app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
   app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
 
 
-def recv_socket_data(c_socket, timeout=10):
-  c_socket.setblocking(0)
-  total_data=[];
-  data='';
-  begin=time.time()
-  while True:
-    if total_data and time.time()-begin > timeout:
-      break
-    elif time.time()-begin > timeout*2:
-      break
-    try:
-      data = c_socket.recv(8192)
-      if data:
-        total_data.append(data)
-        #change the beginning time for measurement
-        begin=time.time()
-      else:
-        #sleep for sometime to indicate a gap
-        time.sleep(0.1)
-        break
-    except:
-      pass
-  return ''.join(total_data)
-
-def exec_run_handler(type, output):
-  if type == 'generic':
-    if output.exit_code == 0:
-      return jsonify(type='success', msg='command completed successfully')
-    else:
-      return jsonify(type='danger', msg='command failed: ' + output.output)
-  if type == 'utf8_text_only':
-    r = Response(response=output.output, status=200, mimetype="text/plain")
-    r.headers["Content-Type"] = "text/plain; charset=utf-8"
-    return r
-
-def create_self_signed_cert():
-  success = False
-  while not success:
-    try:
-      pkey = crypto.PKey()
-      pkey.generate_key(crypto.TYPE_RSA, 2048)
-      cert = crypto.X509()
-      cert.get_subject().O = "mailcow"
-      cert.get_subject().CN = "dockerapi"
-      cert.set_serial_number(int(uuid.uuid4()))
-      cert.gmtime_adj_notBefore(0)
-      cert.gmtime_adj_notAfter(10*365*24*60*60)
-      cert.set_issuer(cert.get_subject())
-      cert.set_pubkey(pkey)
-      cert.sign(pkey, 'sha512')
-      cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
-      pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
-      with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle:
-        handle.write(cert)
-      with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
-        handle.write(pkey)
-      success = True
-    except:
-      time.sleep(1)
-      try:
-        os.remove('/cert.pem')
-        os.remove('/key.pem')
-      except OSError:
-        pass
-
 api.add_resource(containers_get, '/containers/json')
 api.add_resource(containers_get, '/containers/json')
-api.add_resource(container_get, '/containers/<string:container_id>/json')
+api.add_resource(container_get,  '/containers/<string:container_id>/json')
 api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
 api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
@@ -397,5 +397,4 @@ if __name__ == '__main__':
     time.sleep(1)
     time.sleep(1)
     if killer.kill_now:
     if killer.kill_now:
       break
       break
-  print "Stopping dockerapi-mailcow"
-
+  print ("Stopping dockerapi-mailcow")

+ 61 - 66
data/Dockerfiles/dovecot/Dockerfile

@@ -3,117 +3,112 @@ 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
 
 
-RUN apt-get update && apt-get -y --no-install-recommends install \
-  automake \
-  autotools-dev \
-  build-essential \
+# Add groups and users before installing Dovecot to not break compatibility
+RUN groupadd -g 5000 vmail \
+  && groupadd -g 401 dovecot \
+  && groupadd -g 402 dovenull \
+  && useradd -g vmail -u 5000 vmail -d /var/vmail \
+  && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
+  && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
+  && touch /etc/default/locale \
+  && apt-get update \
+  && apt-get -y --no-install-recommends install \
+  apt-transport-https \
   ca-certificates \
   ca-certificates \
   cpanminus \
   cpanminus \
+  cron \
   curl \
   curl \
-  default-libmysqlclient-dev \
   dnsutils \
   dnsutils \
+  dirmngr \
   gettext \
   gettext \
+  gnupg2 \
   jq \
   jq \
-  libjson-webtoken-perl \
+  libauthen-ntlm-perl \
   libcgi-pm-perl \
   libcgi-pm-perl \
   libcrypt-openssl-rsa-perl \
   libcrypt-openssl-rsa-perl \
-  libdata-uniqid-perl \
-  libhtml-parser-perl \
-  libmail-imapclient-perl \
-  libparse-recdescent-perl \
-  libsys-meminfo-perl \
-  libtest-mockobject-perl \
-  libwww-perl \
-  libauthen-ntlm-perl \
-  libbz2-dev \
   libcrypt-ssleay-perl \
   libcrypt-ssleay-perl \
-  libcurl4-openssl-dev \
+  libdata-uniqid-perl \
   libdbd-mysql-perl \
   libdbd-mysql-perl \
   libdbi-perl \
   libdbi-perl \
   libdigest-hmac-perl \
   libdigest-hmac-perl \
-  libexpat1-dev \
+  libdist-checkconflicts-perl \
   libfile-copy-recursive-perl \
   libfile-copy-recursive-perl \
+  libfile-tail-perl \
+  libhtml-parser-perl \
   libio-compress-perl \
   libio-compress-perl \
   libio-socket-inet6-perl \
   libio-socket-inet6-perl \
   libio-socket-ssl-perl \
   libio-socket-ssl-perl \
   libio-tee-perl \
   libio-tee-perl \
   libipc-run-perl \
   libipc-run-perl \
-  libldap2-dev \
+  libjson-webtoken-perl \
   liblockfile-simple-perl \
   liblockfile-simple-perl \
-  liblz-dev \
-  liblz4-dev \
-  liblzma-dev \
+  libmail-imapclient-perl \
+  libmodule-implementation-perl \
   libmodule-scandeps-perl \
   libmodule-scandeps-perl \
   libnet-ssleay-perl \
   libnet-ssleay-perl \
-  libpam-dev \
+  libpackage-stash-perl \
+  libpackage-stash-xs-perl \
   libpar-packer-perl \
   libpar-packer-perl \
+  libparse-recdescent-perl \
+  libproc-processtable-perl \
   libreadonly-perl \
   libreadonly-perl \
-  libssl-dev \
+  libregexp-common-perl \
+  libsys-meminfo-perl \
   libterm-readkey-perl \
   libterm-readkey-perl \
+  libtest-deep-perl \
+  libtest-fatal-perl \
+  libtest-mock-guard-perl \
+  libtest-mockobject-perl \
+  libtest-nowarnings-perl \
   libtest-pod-perl \
   libtest-pod-perl \
+  libtest-requires-perl \
   libtest-simple-perl \
   libtest-simple-perl \
+  libtest-warn-perl \
   libtry-tiny-perl \
   libtry-tiny-perl \
   libunicode-string-perl \
   libunicode-string-perl \
-  libproc-processtable-perl \
-  libtest-nowarnings-perl \
-  libtest-deep-perl \
-  libtest-warn-perl \
-  libregexp-common-perl \
   liburi-perl \
   liburi-perl \
-  lzma-dev \
+  libwww-perl \
+  mysql-client \
+  procps \
   python-html2text \
   python-html2text \
   python-jinja2 \
   python-jinja2 \
   python-mysql.connector \
   python-mysql.connector \
   python-redis \
   python-redis \
-  make \
-  mysql-client \
-  procps \
-  supervisor \
-  cron \
   redis-server \
   redis-server \
+  supervisor \
   syslog-ng \
   syslog-ng \
   syslog-ng-core \
   syslog-ng-core \
   syslog-ng-mod-redis \
   syslog-ng-mod-redis \
-  && rm -rf /var/lib/apt/lists/* \
-  && curl https://www.dovecot.org/releases/2.3/dovecot-$DOVECOT_VERSION.tar.gz | tar xvz  \
-  && cd dovecot-$DOVECOT_VERSION \
-  && ./configure --with-solr --with-mysql --with-ldap --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib --enable-hardening \
-  && make -j3 \
-  && make install \
-  && make clean \
-  && cd .. && rm -rf dovecot-$DOVECOT_VERSION \
-  && curl https://pigeonhole.dovecot.org/releases/2.3/dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION.tar.gz | tar xvz  \
-  && cd dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION \
-  && ./configure \
-  && make -j3 \
-  && make install \
-  && make clean \
-  && cd .. \
-  && rm -rf dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION \
-  && cpanm Data::Uniqid Mail::IMAPClient String::Util \
-  && groupadd -g 5000 vmail \
-  && groupadd -g 401 dovecot \
-  && groupadd -g 402 dovenull \
-  && useradd -g vmail -u 5000 vmail -d /var/vmail \
-  && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
-  && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
-  && touch /etc/default/locale \
-  && apt-get purge -y build-essential automake autotools-dev default-libmysqlclient-dev libbz2-dev libcurl4-openssl-dev libexpat1-dev liblz-dev liblz4-dev liblzma-dev libpam-dev libssl-dev lzma-dev \
+  && apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
+  && echo 'deb https://repo.dovecot.org/ce-2.3-latest/debian/stretch stretch main' > /etc/apt/sources.list.d/dovecot.list \
+  && apt-get update \
+  && apt-get -y --no-install-recommends install \
+  dovecot-lua \
+  dovecot-managesieved \
+  dovecot-sieve \
+  dovecot-lmtpd \
+  dovecot-ldap \
+  dovecot-mysql \
+  dovecot-core \
+  dovecot-pop3d \
+  dovecot-imapd \
+  dovecot-solr \
   && apt-get autoremove --purge -y \
   && apt-get autoremove --purge -y \
-  && rm -rf /tmp/* /var/tmp/*
+  && apt-get autoclean \
+  && rm -rf /var/lib/apt/lists/* \
+  && rm -rf /tmp/* /var/tmp/* /etc/cron.daily/*
 
 
 COPY trim_logs.sh /usr/local/bin/trim_logs.sh
 COPY trim_logs.sh /usr/local/bin/trim_logs.sh
+COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
 COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
 COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
 COPY imapsync /usr/local/bin/imapsync
 COPY imapsync /usr/local/bin/imapsync
 COPY postlogin.sh /usr/local/bin/postlogin.sh
 COPY postlogin.sh /usr/local/bin/postlogin.sh
 COPY imapsync_cron.pl /usr/local/bin/imapsync_cron.pl
 COPY imapsync_cron.pl /usr/local/bin/imapsync_cron.pl
-COPY report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve
-COPY report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve
-COPY rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham
-COPY rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam
+COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
+COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
+COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
+COPY rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam
 COPY sa-rules.sh /usr/local/bin/sa-rules.sh
 COPY sa-rules.sh /usr/local/bin/sa-rules.sh
 COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh
 COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh
 COPY docker-entrypoint.sh /
 COPY docker-entrypoint.sh /

+ 18 - 0
data/Dockerfiles/dovecot/clean_q_aged.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+MAX_AGE=$(redis-cli --raw -h redis-mailcow GET Q_MAX_AGE)
+
+if [[ -z ${MAX_AGE} ]]; then
+  echo "Max age for quarantine items not defined"
+  exit 1
+fi
+
+NUM_REGEXP='^[0-9]+$'
+if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then
+  echo "Max age for quarantine items invalid"
+  exit 1
+fi
+
+TO_DELETE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u __DBUSER__ -p__DBPASS__ __DBNAME__ -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
+mysql --socket=/var/run/mysqld/mysqld.sock -u __DBUSER__ -p__DBPASS__ __DBNAME__ -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
+echo "Deleted ${TO_DELETE} items from quarantine table (max age is ${MAX_AGE//[!0-9]/} days)"

+ 97 - 31
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -16,10 +16,14 @@ sed -i "s/__DBUSER__/${DBUSER}/g" /usr/local/bin/quarantine_notify.py
 sed -i "s/__DBPASS__/${DBPASS}/g" /usr/local/bin/quarantine_notify.py
 sed -i "s/__DBPASS__/${DBPASS}/g" /usr/local/bin/quarantine_notify.py
 sed -i "s/__DBNAME__/${DBNAME}/g" /usr/local/bin/quarantine_notify.py
 sed -i "s/__DBNAME__/${DBNAME}/g" /usr/local/bin/quarantine_notify.py
 
 
+sed -i "s/__DBUSER__/${DBUSER}/g" /usr/local/bin/clean_q_aged.sh
+sed -i "s/__DBPASS__/${DBPASS}/g" /usr/local/bin/clean_q_aged.sh
+sed -i "s/__DBNAME__/${DBNAME}/g" /usr/local/bin/clean_q_aged.sh
+
 sed -i "s/__LOG_LINES__/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh
 sed -i "s/__LOG_LINES__/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh
 
 
 # Create missing directories
 # Create missing directories
-[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/
+[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
 [[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
 [[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
 [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
 [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
 [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
 [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
@@ -29,7 +33,8 @@ sed -i "s/__LOG_LINES__/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh
 DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 
 
 # Create quota dict for Dovecot
 # Create quota dict for Dovecot
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf
+cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
+# Autogenerated by mailcow
 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}"
 map {
 map {
   pattern = priv/quota/storage
   pattern = priv/quota/storage
@@ -46,7 +51,8 @@ map {
 EOF
 EOF
 
 
 # Create dict used for sieve pre and postfilters
 # Create dict used for sieve pre and postfilters
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
+cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
+# Autogenerated by mailcow
 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}"
 map {
 map {
   pattern = priv/sieve/name/\$script_name
   pattern = priv/sieve/name/\$script_name
@@ -68,7 +74,8 @@ map {
 }
 }
 EOF
 EOF
 
 
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
+cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
+# Autogenerated by mailcow
 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}"
 map {
 map {
   pattern = priv/sieve/name/\$script_name
   pattern = priv/sieve/name/\$script_name
@@ -90,36 +97,41 @@ map {
 }
 }
 EOF
 EOF
 
 
-echo -n ${ACL_ANYONE} > /usr/local/etc/dovecot/acl_anyone
+echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
 
 
 if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
 if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify' > /usr/local/etc/dovecot/mail_plugins
-echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log' > /usr/local/etc/dovecot/mail_plugins_imap
-echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl' > /usr/local/etc/dovecot/mail_plugins_lmtp
+echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify' > /etc/dovecot/mail_plugins
+echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log' > /etc/dovecot/mail_plugins_imap
+echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl' > /etc/dovecot/mail_plugins_lmtp
 else
 else
-echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr' > /usr/local/etc/dovecot/mail_plugins
-echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr' > /usr/local/etc/dovecot/mail_plugins_imap
-echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr' > /usr/local/etc/dovecot/mail_plugins_lmtp
+echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr' > /etc/dovecot/mail_plugins
+echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr' > /etc/dovecot/mail_plugins_imap
+echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr' > /etc/dovecot/mail_plugins_lmtp
 fi
 fi
-chmod 644 /usr/local/etc/dovecot/mail_plugins /usr/local/etc/dovecot/mail_plugins_imap /usr/local/etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
+chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
 
 
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf
+cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
+# Autogenerated by mailcow
 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
 
 
 # Create pass dict for Dovecot
 # Create pass dict for Dovecot
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf
+cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-passdb.conf
+# Autogenerated by mailcow
 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}"
 default_pass_scheme = SSHA256
 default_pass_scheme = SSHA256
 password_query = SELECT password FROM mailbox WHERE active = '1' AND username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_EXTRACT(attributes, '$.force_pw_update') NOT LIKE '%%1%%'
 password_query = SELECT password FROM mailbox WHERE active = '1' AND username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_EXTRACT(attributes, '$.force_pw_update') NOT LIKE '%%1%%'
 EOF
 EOF
 
 
-# Create global sieve_after script
-cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve
+# Migrate old sieve_after file
+[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
+# Create global sieve scripts
+cat /etc/dovecot/global_sieve_after > /var/vmail/sieve/global_sieve_after.sieve
+cat /etc/dovecot/global_sieve_before > /var/vmail/sieve/global_sieve_before.sieve
 
 
 # Check permissions of vmail/attachments directory.
 # Check permissions of vmail/attachments directory.
 # Do not do this every start-up, it may take a very long time. So we use a stat check here.
 # Do not do this every start-up, it may take a very long time. So we use a stat check here.
@@ -127,14 +139,51 @@ 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)
 
 
-echo ${RAND_USER}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}') > /usr/local/etc/dovecot/dovecot-master.passwd
-echo ${RAND_USER}@mailcow.local::5000:5000:::: > /usr/local/etc/dovecot/dovecot-master.userdb
+echo ${RAND_USER}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}') > /etc/dovecot/dovecot-master.passwd
+echo ${RAND_USER}@mailcow.local::5000:5000:::: > /etc/dovecot/dovecot-master.userdb
 echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds
 echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds
 
 
+if [[ -z ${MAILDIR_SUB} ]]; then
+  MAILDIR_SUB_SHARED=
+else
+  MAILDIR_SUB_SHARED=/${MAILDIR_SUB}
+fi
+cat <<EOF > /etc/dovecot/shared_namespace.conf
+# Autogenerated by mailcow
+namespace {
+    type = shared
+    separator = /
+    prefix = Shared/%%u/
+    location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u;CONTROL=~${MAILDIR_SUB_SHARED}/Shared/%%u
+    subscriptions = no
+    list = children
+}
+EOF
+
+if [[ "${ALLOW_ADMIN_EMAIL_LOGIN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+    # Create random master Password for SOGo 'login as user' via proxy auth
+    RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
+    echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
+    cat <<EOF > /etc/dovecot/sogo-sso.conf
+# Autogenerated by mailcow
+passdb {
+  driver = static
+  args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
+}
+EOF
+else
+    rm -f /etc/dovecot/sogo-sso.pass
+    rm -f /etc/dovecot/sogo-sso.conf
+fi
+
 # 401 is user dovecot
 # 401 is user dovecot
 if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
 if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
 	openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
 	openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
@@ -145,43 +194,46 @@ else
 fi
 fi
 
 
 # Compile sieve scripts
 # Compile sieve scripts
-sievec /var/vmail/sieve/global.sieve
-sievec /usr/local/lib/dovecot/sieve/report-spam.sieve
-sievec /usr/local/lib/dovecot/sieve/report-ham.sieve
+sievec /var/vmail/sieve/global_sieve_before.sieve
+sievec /var/vmail/sieve/global_sieve_after.sieve
+sievec /usr/lib/dovecot/sieve/report-spam.sieve
+sievec /usr/lib/dovecot/sieve/report-ham.sieve
 
 
 # Fix permissions
 # Fix permissions
-chown root:root /usr/local/etc/dovecot/sql/*.conf
-chown root:dovecot /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve* /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota*
-chmod 640 /usr/local/etc/dovecot/sql/*.conf
+chown root:root /etc/dovecot/sql/*.conf
+chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota*
+chmod 640 /etc/dovecot/sql/*.conf
 chown -R vmail:vmail /var/vmail/sieve
 chown -R vmail:vmail /var/vmail/sieve
 chown -R vmail:vmail /var/volatile
 chown -R vmail:vmail /var/volatile
 adduser vmail tty
 adduser vmail tty
 chmod g+rw /dev/console
 chmod g+rw /dev/console
-chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \
-  /usr/local/lib/dovecot/sieve/rspamd-pipe-spam \
+chown root:tty /dev/console
+chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
+  /usr/lib/dovecot/sieve/rspamd-pipe-spam \
   /usr/local/bin/imapsync_cron.pl \
   /usr/local/bin/imapsync_cron.pl \
   /usr/local/bin/postlogin.sh \
   /usr/local/bin/postlogin.sh \
   /usr/local/bin/imapsync \
   /usr/local/bin/imapsync \
   /usr/local/bin/trim_logs.sh \
   /usr/local/bin/trim_logs.sh \
   /usr/local/bin/sa-rules.sh \
   /usr/local/bin/sa-rules.sh \
+  /usr/local/bin/clean_q_aged.sh \
   /usr/local/bin/maildir_gc.sh \
   /usr/local/bin/maildir_gc.sh \
   /usr/local/sbin/stop-supervisor.sh \
   /usr/local/sbin/stop-supervisor.sh \
   /usr/local/bin/quota_notify.py
   /usr/local/bin/quota_notify.py
 
 
 # Setup cronjobs
 # Setup cronjobs
 echo '* * * * *    root  /usr/local/bin/imapsync_cron.pl 2>&1 | /usr/bin/logger' > /etc/cron.d/imapsync
 echo '* * * * *    root  /usr/local/bin/imapsync_cron.pl 2>&1 | /usr/bin/logger' > /etc/cron.d/imapsync
-echo '30 3 * * *   vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync
+#echo '30 3 * * *   vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync
 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
-
+echo '15 4 * * * vmail /usr/local/bin/clean_q_aged.sh >> /dev/console 2>&1' > /etc/cron.d/clean_q_aged
 # Fix more than 1 hardlink issue
 # Fix more than 1 hardlink issue
 touch /etc/crontab /etc/cron.*/*
 touch /etc/crontab /etc/cron.*/*
 
 
 # Clean old PID if any
 # Clean old PID if any
-[[ -f /usr/local/var/run/dovecot/master.pid ]] && rm /usr/local/var/run/dovecot/master.pid
+[[ -f /var/run/dovecot/master.pid ]] && rm /var/run/dovecot/master.pid
 
 
 # Clean stopped imapsync jobs
 # Clean stopped imapsync jobs
 rm -f /tmp/imapsync_busy.lock
 rm -f /tmp/imapsync_busy.lock
@@ -191,6 +243,20 @@ IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBP
 # Envsubst maildir_gc
 # Envsubst maildir_gc
 echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
 echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
 
 
+PUBKEY_MCRYPT=$(doveconf -P | grep -i mail_crypt_global_public_key | cut -d '<' -f2)
+if [ -f ${PUBKEY_MCRYPT} ]; then
+  GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
+  if [ ${#GUID} -eq 64 ]; then
+    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
+EOF
+  else
+    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
+EOF
+  fi
+fi
+
 # Collect SA rules once now
 # Collect SA rules once now
 /usr/local/bin/sa-rules.sh
 /usr/local/bin/sa-rules.sh
 
 

File diff suppressed because it is too large
+ 296 - 187
data/Dockerfiles/dovecot/imapsync


+ 45 - 30
data/Dockerfiles/dovecot/imapsync_cron.pl

@@ -5,11 +5,11 @@ use LockFile::Simple qw(lock trylock unlock);
 use Proc::ProcessTable;
 use Proc::ProcessTable;
 use Data::Dumper qw(Dumper);
 use Data::Dumper qw(Dumper);
 use IPC::Run 'run';
 use IPC::Run 'run';
-use String::Util 'trim';
 use File::Temp;
 use File::Temp;
 use Try::Tiny;
 use Try::Tiny;
 use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
 use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
 
 
+sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s };
 my $t = Proc::ProcessTable->new;
 my $t = Proc::ProcessTable->new;
 my $imapsync_running = grep { $_->{cmndline} =~ /^\/usr\/bin\/perl \/usr\/local\/bin\/imapsync\s/ } @{$t->table};
 my $imapsync_running = grep { $_->{cmndline} =~ /^\/usr\/bin\/perl \/usr\/local\/bin\/imapsync\s/ } @{$t->table};
 if ($imapsync_running eq 1)
 if ($imapsync_running eq 1)
@@ -19,11 +19,20 @@ if ($imapsync_running eq 1)
 }
 }
 
 
 sub qqw($) {
 sub qqw($) {
-  my @values = split('(?=--)', $_[0]);
+  my @params = ();
+  my @values = split(/(?=--)/, $_[0]);
   foreach my $val (@values) {
   foreach my $val (@values) {
+    my @tmpparam = split(/ /, $val, 2);
+    foreach my $tmpval (@tmpparam) {
+        if ($tmpval ne '') {
+          push @params, $tmpval;
+        }
+    }
+  }
+  foreach my $val (@params) {
     $val=trim($val);
     $val=trim($val);
   }
   }
-  return @values
+  return @params;
 }
 }
 
 
 $run_dir="/tmp";
 $run_dir="/tmp";
@@ -101,10 +110,6 @@ while ($row = $sth->fetchrow_arrayref()) {
   $timeout1            = @$row[19];
   $timeout1            = @$row[19];
   $timeout2            = @$row[20];
   $timeout2            = @$row[20];
 
 
-  $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?");
-  $is_running->bind_param( 1, ${id} );
-  $is_running->execute();
-
   if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
   if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
 
 
   my $template = $run_dir . '/imapsync.XXXXXXX';
   my $template = $run_dir . '/imapsync.XXXXXXX';
@@ -118,43 +123,53 @@ while ($row = $sth->fetchrow_arrayref()) {
   my $custom_params_ref = \@custom_params_a;
   my $custom_params_ref = \@custom_params_a;
 
 
   my $generated_cmds = [ "/usr/local/bin/imapsync",
   my $generated_cmds = [ "/usr/local/bin/imapsync",
-	"--tmpdir", "/tmp",
-	"--nofoldersizes",
-	($timeout1 gt "0" ? () : ('--timeout1', $timeout1)),
-	($timeout2 gt "0" ? () : ('--timeout2', $timeout2)),
-	($exclude eq ""	? () : ("--exclude", $exclude)),
-	($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
-	($maxage eq "0" ? () : ('--maxage', $maxage)),
-	($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
-	($delete2duplicates	ne "1" ? () : ('--delete2duplicates')),
-	($subscribeall	ne "1" ? () : ('--subscribeall')),
-	($delete1	ne "1" ? () : ('--delete')),
+  "--tmpdir", "/tmp",
+  "--nofoldersizes",
+  ($timeout1 gt "0" ? () : ('--timeout1', $timeout1)),
+  ($timeout2 gt "0" ? () : ('--timeout2', $timeout2)),
+  ($exclude eq "" ? () : ("--exclude", $exclude)),
+  ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
+  ($maxage eq "0" ? () : ('--maxage', $maxage)),
+  ($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
+  ($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
+  ($subscribeall  ne "1" ? () : ('--subscribeall')),
+  ($delete1 ne "1" ? () : ('--delete')),
   ($delete2 ne "1" ? () : ('--delete2')),
   ($delete2 ne "1" ? () : ('--delete2')),
   ($automap ne "1" ? () : ('--automap')),
   ($automap ne "1" ? () : ('--automap')),
   ($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')),
   ($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')),
-	(!defined($enc1) ? () : ($enc1)),
-	"--host1", $host1,
-	"--user1", $user1,
-	"--passfile1", $passfile1->filename,
-	"--port1", $port1,
-	"--host2", "localhost",
-	"--user2", $user2 . '*' . trim($master_user),
-	"--passfile2", $passfile2->filename,
-	'--no-modulesversion'];
+  (!defined($enc1) ? () : ($enc1)),
+  "--host1", $host1,
+  "--user1", $user1,
+  "--passfile1", $passfile1->filename,
+  "--port1", $port1,
+  "--host2", "localhost",
+  "--user2", $user2 . '*' . trim($master_user),
+  "--passfile2", $passfile2->filename,
+  '--no-modulesversion',
+  '--noreleasecheck'];
 
 
   try {
   try {
+    $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?");
+    $is_running->bind_param( 1, ${id} );
+    $is_running->execute();
+    
     run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout;
     run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout;
-    $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, last_run = NOW(), is_running = 0 WHERE id = ?");
+    
+    $update = $dbh->prepare("UPDATE imapsync SET returned_text = ? WHERE id = ?");
     $update->bind_param( 1, ${stdout} );
     $update->bind_param( 1, ${stdout} );
     $update->bind_param( 2, ${id} );
     $update->bind_param( 2, ${id} );
     $update->execute();
     $update->execute();
   } catch {
   } catch {
-    $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', last_run = NOW(), is_running = 0 WHERE id = ?");
+    $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync' WHERE id = ?");
+    $update->bind_param( 1, ${id} );
+    $update->execute();
+  } finally {
+    $update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?");
     $update->bind_param( 1, ${id} );
     $update->bind_param( 1, ${id} );
     $update->execute();
     $update->execute();
-    $lockmgr->unlock($lock_file);
   };
   };
 
 
+
 }
 }
 
 
 $sth->finish();
 $sth->finish();

+ 2 - 1
data/Dockerfiles/dovecot/quarantine_notify.py

@@ -83,13 +83,14 @@ def notify_rcpt(rcpt, msg_count, quarantine_acl):
       msg.attach(html_part)
       msg.attach(html_part)
       msg['To'] = str(rcpt)
       msg['To'] = str(rcpt)
       text = msg.as_string()
       text = msg.as_string()
-      server.sendmail(msg['From'], msg['To'], text)
+      server.sendmail(msg['From'].encode("ascii", errors="ignore"), msg['To'], text)
       server.quit()
       server.quit()
       for res in meta_query:
       for res in meta_query:
         query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
         query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
       r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
       r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
       break
       break
     except Exception as ex:
     except Exception as ex:
+      server.quit()
       print '%s'  % (ex)
       print '%s'  % (ex)
       time.sleep(3)
       time.sleep(3)
 
 

+ 1 - 1
data/Dockerfiles/dovecot/quota_notify.py

@@ -54,7 +54,7 @@ try:
   msg.attach(text_part)
   msg.attach(text_part)
   msg.attach(html_part)
   msg.attach(html_part)
   msg['To'] = username
   msg['To'] = username
-  p = Popen(['/usr/local/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
+  p = Popen(['/usr/lib/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
   p.communicate(input=msg.as_string())
   p.communicate(input=msg.as_string())
 
 
 except Exception as ex:
 except Exception as ex:

+ 33 - 17
data/Dockerfiles/dovecot/sa-rules.sh

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

+ 1 - 1
data/Dockerfiles/dovecot/supervisord.conf

@@ -12,7 +12,7 @@ stderr_logfile_maxbytes=0
 autostart=true
 autostart=true
 
 
 [program:dovecot]
 [program:dovecot]
-command=/usr/local/sbin/dovecot -F
+command=/usr/sbin/dovecot -F
 autorestart=true
 autorestart=true
 
 
 [program:cron]
 [program:cron]

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

@@ -31,10 +31,10 @@ destination d_redis_f2b_channel {
   );
   );
 };
 };
 filter f_mail { facility(mail); };
 filter f_mail { facility(mail); };
-filter f_not_watchdog { not message("172\.22\.1\.248"); };
+#filter f_not_watchdog { not message("172\.22\.1\.248"); };
 log {
 log {
   source(s_src);
   source(s_src);
-  filter(f_not_watchdog);
+#  filter(f_not_watchdog);
   destination(d_stdout);
   destination(d_stdout);
   filter(f_mail);
   filter(f_mail);
   destination(d_redis_ui_log);
   destination(d_redis_ui_log);

+ 1 - 1
data/Dockerfiles/dovecot/trim_logs.sh

@@ -15,4 +15,4 @@ catch_non_zero "/usr/bin/redis-cli -h redis LTRIM NETFILTER_LOG 0 __LOG_LINES__"
 catch_non_zero "/usr/bin/redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 __LOG_LINES__"
 catch_non_zero "/usr/bin/redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 __LOG_LINES__"
 catch_non_zero "/usr/bin/redis-cli -h redis LTRIM API_LOG 0 __LOG_LINES__"
 catch_non_zero "/usr/bin/redis-cli -h redis LTRIM API_LOG 0 __LOG_LINES__"
 catch_non_zero "/usr/bin/redis-cli -h redis LTRIM RL_LOG 0 __LOG_LINES__"
 catch_non_zero "/usr/bin/redis-cli -h redis LTRIM RL_LOG 0 __LOG_LINES__"
-
+catch_non_zero "/usr/bin/redis-cli -h redis LTRIM WATCHDOG_LOG 0 __LOG_LINES__"

+ 8 - 5
data/Dockerfiles/netfilter/Dockerfile

@@ -1,13 +1,16 @@
-FROM alpine:3.9
+FROM alpine:3.10
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
 ENV XTABLES_LIBDIR /usr/lib/xtables
 ENV XTABLES_LIBDIR /usr/lib/xtables
 ENV PYTHON_IPTABLES_XTABLES_VERSION 12
 ENV PYTHON_IPTABLES_XTABLES_VERSION 12
 ENV IPTABLES_LIBDIR /usr/lib
 ENV IPTABLES_LIBDIR /usr/lib
 
 
-RUN apk add -U python2 python-dev py-pip gcc musl-dev iptables ip6tables tzdata \
-  && pip2 install --upgrade python-iptables==0.13.0 redis ipaddress \
-  && apk del python-dev py2-pip gcc
+RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.9/main' >> /etc/apk/repositories \
+  && apk add --virtual .build-deps gcc python3-dev libffi-dev openssl-dev \
+  && apk add -U python3 iptables=1.6.2-r1 ip6tables=1.6.2-r1 tzdata musl-dev \
+  && pip3 install --upgrade pip python-iptables redis ipaddress dnspython \
+#  && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
+  && apk del .build-deps
 
 
 COPY server.py /
 COPY server.py /
-CMD ["python2", "-u", "/server.py"]
+CMD ["python3", "-u", "/server.py"]

+ 202 - 117
data/Dockerfiles/netfilter/server.py

@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 
 
 import re
 import re
 import os
 import os
@@ -6,19 +6,22 @@ import time
 import atexit
 import atexit
 import signal
 import signal
 import ipaddress
 import ipaddress
+from collections import Counter
 from random import randint
 from random import randint
 from threading import Thread
 from threading import Thread
 from threading import Lock
 from threading import Lock
 import redis
 import redis
 import json
 import json
 import iptc
 import iptc
+import dns.resolver
+import dns.exception
 
 
 while True:
 while True:
   try:
   try:
     r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
     r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
     r.ping()
     r.ping()
   except Exception as ex:
   except Exception as ex:
-    print '%s - trying again in 3 seconds'  % (ex)
+    print('%s - trying again in 3 seconds'  % (ex))
     time.sleep(3)
     time.sleep(3)
   else:
   else:
     break
     break
@@ -31,13 +34,34 @@ RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([
 RULES[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
 RULES[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
 RULES[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
 RULES[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
 RULES[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
 RULES[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
-#RULES[6] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+RULES[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
+#RULES[7] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+
+WHITELIST = []
+BLACKLIST= []
 
 
 bans = {}
 bans = {}
-log = {}
+
 quit_now = False
 quit_now = False
 lock = Lock()
 lock = Lock()
 
 
+def log(priority, message):
+  tolog = {}
+  tolog['time'] = int(round(time.time()))
+  tolog['priority'] = priority
+  tolog['message'] = message
+  r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
+  print(message)
+  
+def logWarn(message):
+  log('warn', message)
+  
+def logCrit(message):
+  log('crit', message)
+  
+def logInfo(message):
+  log('info', message)
+  
 def refreshF2boptions():
 def refreshF2boptions():
   global f2boptions
   global f2boptions
   global quit_now
   global quit_now
@@ -58,8 +82,8 @@ def refreshF2boptions():
     try:
     try:
       f2boptions = {}
       f2boptions = {}
       f2boptions = json.loads(r.get('F2B_OPTIONS'))
       f2boptions = json.loads(r.get('F2B_OPTIONS'))
-    except ValueError, e:
-      print 'Error loading F2B options: F2B_OPTIONS is not json'
+    except ValueError:
+      print('Error loading F2B options: F2B_OPTIONS is not json')
       quit_now = True
       quit_now = True
 
 
 if r.exists('F2B_LOG'):
 if r.exists('F2B_LOG'):
@@ -84,18 +108,10 @@ def mailcowChainOrder():
             if item.target.name == 'MAILCOW':
             if item.target.name == 'MAILCOW':
               target_found = True
               target_found = True
               if position != 0:
               if position != 0:
-                log['time'] = int(round(time.time()))
-                log['priority'] = 'crit'
-                log['message'] = 'Error in ' + chain.name + ' chain order, restarting container'
-                r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-                print log['message']
+                logCrit('Error in %s chain order, restarting container' % (chain.name))
                 quit_now = True
                 quit_now = True
           if not target_found:
           if not target_found:
-            log['time'] = int(round(time.time()))
-            log['priority'] = 'crit'
-            log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container'
-            r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-            print log['message']
+            logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
             quit_now = True
             quit_now = True
 
 
 def ban(address):
 def ban(address):
@@ -106,28 +122,28 @@ def ban(address):
   RETRY_WINDOW = int(f2boptions['retry_window'])
   RETRY_WINDOW = int(f2boptions['retry_window'])
   NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
   NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
   NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
   NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
-  WHITELIST = r.hgetall('F2B_WHITELIST')
 
 
-  ip = ipaddress.ip_address(address.decode('ascii'))
+  ip = ipaddress.ip_address(address)
   if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
   if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
     ip = ip.ipv4_mapped
     ip = ip.ipv4_mapped
     address = str(ip)
     address = str(ip)
   if ip.is_private or ip.is_loopback:
   if ip.is_private or ip.is_loopback:
     return
     return
 
 
-  self_network = ipaddress.ip_network(address.decode('ascii'))
-  if WHITELIST:
-    for wl_key in WHITELIST:
-      wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
+  self_network = ipaddress.ip_network(address)
+  
+  with lock:
+    temp_whitelist = set(WHITELIST)
+
+  if temp_whitelist:
+    for wl_key in temp_whitelist:
+      wl_net = ipaddress.ip_network(wl_key, False)
+          
       if wl_net.overlaps(self_network):
       if wl_net.overlaps(self_network):
-        log['time'] = int(round(time.time()))
-        log['priority'] = 'info'
-        log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
-        r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-        print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
+        logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
         return
         return
 
 
-  net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
+  net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
   net = str(net)
   net = str(net)
 
 
   if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
   if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
@@ -142,11 +158,8 @@ def ban(address):
   active_window = time.time() - bans[net]['last_attempt']
   active_window = time.time() - bans[net]['last_attempt']
 
 
   if bans[net]['attempts'] >= MAX_ATTEMPTS:
   if bans[net]['attempts'] >= MAX_ATTEMPTS:
-    log['time'] = int(round(time.time()))
-    log['priority'] = 'crit'
-    log['message'] = 'Banning %s' % net
-    r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-    print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
+    cur_time = int(round(time.time()))
+    logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
     if type(ip) is ipaddress.IPv4Address:
     if type(ip) is ipaddress.IPv4Address:
       with lock:
       with lock:
         chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
         chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
@@ -165,29 +178,18 @@ def ban(address):
         rule.target = target
         rule.target = target
         if rule not in chain.rules:
         if rule not in chain.rules:
           chain.insert_rule(rule)
           chain.insert_rule(rule)
-    r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
+    r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
   else:
   else:
-    log['time'] = int(round(time.time()))
-    log['priority'] = 'warn'
-    log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
-    r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-    print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
+    logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
 
 
 def unban(net):
 def unban(net):
   global lock
   global lock
-  log['time'] = int(round(time.time())) 
-  log['priority'] = 'info'
-  r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
   if not net in bans:
   if not net in bans:
-   log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
-   r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-   print '%s is not banned, skipping unban and deleting from queue (if any)' % net
+   logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
    r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
    r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
    return
    return
-  log['message'] = 'Unbanning %s' % net
-  r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-  print 'Unbanning %s' % net
-  if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
+  logInfo('Unbanning %s' % net)
+  if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
     with lock:
     with lock:
       chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
       chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
       rule = iptc.Rule()
       rule = iptc.Rule()
@@ -210,17 +212,47 @@ def unban(net):
   if net in bans:
   if net in bans:
     del bans[net]
     del bans[net]
 
 
+def permBan(net, unban=False):
+  global lock
+  
+  if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
+    with lock:
+      chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+      rule = iptc.Rule()
+      rule.src = net
+      target = iptc.Target(rule, "REJECT")
+      rule.target = target
+      if rule not in chain.rules and not unban:
+        logCrit('Add host/network %s to blacklist' % net)
+        chain.insert_rule(rule)
+        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) 
+      elif rule in chain.rules and unban:
+        logCrit('Remove host/network %s from blacklist' % net)
+        chain.delete_rule(rule)
+        r.hdel('F2B_PERM_BANS', '%s' % net)
+  else:
+    with lock:
+      chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
+      rule = iptc.Rule6()
+      rule.src = net
+      target = iptc.Target(rule, "REJECT")
+      rule.target = target
+      if rule not in chain.rules and not unban:
+        logCrit('Add host/network %s to blacklist' % net)
+        chain.insert_rule(rule)
+        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) 
+      elif rule in chain.rules and unban:
+        logCrit('Remove host/network %s from blacklist' % net)
+        chain.delete_rule(rule)
+        r.hdel('F2B_PERM_BANS', '%s' % net)
+    
 def quit(signum, frame):
 def quit(signum, frame):
   global quit_now
   global quit_now
   quit_now = True
   quit_now = True
 
 
 def clear():
 def clear():
   global lock
   global lock
-  log['time'] = int(round(time.time()))
-  log['priority'] = 'info'
-  log['message'] = 'Clearing all bans'
-  r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-  print 'Clearing all bans'
+  logInfo('Clearing all bans')
   for net in bans.copy():
   for net in bans.copy():
     unban(net)
     unban(net)
   with lock:
   with lock:
@@ -249,28 +281,20 @@ def clear():
     pubsub.unsubscribe()
     pubsub.unsubscribe()
 
 
 def watch():
 def watch():
-  log['time'] = int(round(time.time()))
-  log['priority'] = 'info'
-  log['message'] = 'Watching Redis channel F2B_CHANNEL'
-  r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
+  logInfo('Watching Redis channel F2B_CHANNEL')
   pubsub.subscribe('F2B_CHANNEL')
   pubsub.subscribe('F2B_CHANNEL')
-  print 'Subscribing to Redis channel F2B_CHANNEL'
 
 
   while not quit_now:
   while not quit_now:
     for item in pubsub.listen():
     for item in pubsub.listen():
-      for rule_id, rule_regex in RULES.iteritems():
+      for rule_id, rule_regex in RULES.items():
         if item['data'] and item['type'] == 'message':
         if item['data'] and item['type'] == 'message':
           result = re.search(rule_regex, item['data'])
           result = re.search(rule_regex, item['data'])
           if result:
           if result:
             addr = result.group(1)
             addr = result.group(1)
-            ip = ipaddress.ip_address(addr.decode('ascii'))
+            ip = ipaddress.ip_address(addr)
             if ip.is_private or ip.is_loopback:
             if ip.is_private or ip.is_loopback:
               continue
               continue
-            print '%s matched rule id %d' % (addr, rule_id)
-            log['time'] = int(round(time.time()))
-            log['priority'] = 'warn'
-            log['message'] = '%s matched rule id %d' % (addr, rule_id)
-            r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
+            logWarn('%s matched rule id %d' % (addr, rule_id))
             ban(addr)
             ban(addr)
 
 
 def snat4(snat_target):
 def snat4(snat_target):
@@ -294,11 +318,7 @@ def snat4(snat_target):
         chain = iptc.Chain(table, 'POSTROUTING')
         chain = iptc.Chain(table, 'POSTROUTING')
         table.autocommit = False
         table.autocommit = False
         if get_snat4_rule() not in chain.rules:
         if get_snat4_rule() not in chain.rules:
-          log['time'] = int(round(time.time()))
-          log['priority'] = 'info'
-          log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target
-          r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-          print log['message']
+          logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target))  
           chain.insert_rule(get_snat4_rule())
           chain.insert_rule(get_snat4_rule())
           table.commit()
           table.commit()
         else:
         else:
@@ -309,7 +329,7 @@ def snat4(snat_target):
           table.commit()
           table.commit()
         table.autocommit = True
         table.autocommit = True
       except:
       except:
-        print 'Error running SNAT4, retrying...' 
+        print('Error running SNAT4, retrying...') 
 
 
 def snat6(snat_target):
 def snat6(snat_target):
   global lock
   global lock
@@ -332,11 +352,7 @@ def snat6(snat_target):
         chain = iptc.Chain(table, 'POSTROUTING')
         chain = iptc.Chain(table, 'POSTROUTING')
         table.autocommit = False
         table.autocommit = False
         if get_snat6_rule() not in chain.rules:
         if get_snat6_rule() not in chain.rules:
-          log['time'] = int(round(time.time()))
-          log['priority'] = 'info'
-          log['message'] = 'Added POSTROUTING rule for source network ' + get_snat6_rule().src + ' to SNAT target ' + snat_target
-          r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-          print log['message']
+          logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
           chain.insert_rule(get_snat6_rule())
           chain.insert_rule(get_snat6_rule())
           table.commit()
           table.commit()
         else:
         else:
@@ -347,14 +363,14 @@ def snat6(snat_target):
           table.commit()
           table.commit()
         table.autocommit = True
         table.autocommit = True
       except:
       except:
-        print 'Error running SNAT6, retrying...' 
+        print('Error running SNAT6, retrying...') 
 
 
 def autopurge():
 def autopurge():
   while not quit_now:
   while not quit_now:
     time.sleep(10)
     time.sleep(10)
     refreshF2boptions()
     refreshF2boptions()
-    BAN_TIME = f2boptions['ban_time']
-    MAX_ATTEMPTS = f2boptions['max_attempts']
+    BAN_TIME = int(f2boptions['ban_time'])
+    MAX_ATTEMPTS = int(f2boptions['max_attempts'])
     QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
     QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
     if QUEUE_UNBAN:
     if QUEUE_UNBAN:
       for net in QUEUE_UNBAN:
       for net in QUEUE_UNBAN:
@@ -364,9 +380,101 @@ def autopurge():
         if time.time() - bans[net]['last_attempt'] > BAN_TIME:
         if time.time() - bans[net]['last_attempt'] > BAN_TIME:
           unban(net)
           unban(net)
 
 
+def isIpNetwork(address):
+  try:
+    ipaddress.ip_network(address, False)
+  except ValueError:
+    return False
+  return True
+
+          
+def genNetworkList(list):
+  resolver = dns.resolver.Resolver()
+  hostnames = []
+  networks = []
+
+  for key in list:
+    if isIpNetwork(key):
+      networks.append(key)
+    else:
+      hostnames.append(key)
+
+  for hostname in hostnames:
+    hostname_ips = []
+    for rdtype in ['A', 'AAAA']:
+      try:
+        answer = resolver.query(qname=hostname, rdtype=rdtype, lifetime=3)
+      except dns.exception.Timeout:
+        logInfo('Hostname %s timedout on resolve' % hostname)
+        break
+      except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+        continue
+      except dns.exception.DNSException as dnsexception:
+        logInfo('%s' % dnsexception)
+        continue
+
+      for rdata in answer:
+        hostname_ips.append(rdata.to_text())
+
+    networks.extend(hostname_ips)
+      
+  return set(networks)
+
+def whitelistUpdate():
+  global lock
+  global quit_now
+  global WHITELIST
+  
+  while not quit_now:
+    start_time = time.time()
+    list = r.hgetall('F2B_WHITELIST')
+    
+    new_whitelist = []
+    
+    if list:
+      new_whitelist = genNetworkList(list)
+    
+    with lock:
+      if Counter(new_whitelist) != Counter(WHITELIST):
+        WHITELIST = new_whitelist
+        logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
+
+    time.sleep(60.0 - ((time.time() - start_time) % 60.0)) 
+    
+def blacklistUpdate():
+  global quit_now
+  global BLACKLIST
+  
+  while not quit_now:
+    start_time = time.time()
+    list = r.hgetall('F2B_BLACKLIST')
+    
+    new_blacklist = []
+    
+    if list:
+      new_blacklist = genNetworkList(list)
+      
+    if Counter(new_blacklist) != Counter(BLACKLIST): 
+      addban = set(new_blacklist).difference(BLACKLIST)
+      delban = set(BLACKLIST).difference(new_blacklist)
+        
+      BLACKLIST = new_blacklist
+      logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
+        
+      if addban:
+        for net in addban:
+          permBan(net=net)
+            
+      if delban:
+        for net in delban:
+          permBan(net=net, unban=True)
+      
+        
+    time.sleep(60.0 - ((time.time() - start_time) % 60.0)) 
+      
 def initChain():
 def initChain():
   # Is called before threads start, no locking
   # Is called before threads start, no locking
-  print "Initializing mailcow netfilter chain"
+  print("Initializing mailcow netfilter chain")
   # IPv4
   # IPv4
   if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
   if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
     iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
     iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
@@ -391,38 +499,7 @@ def initChain():
     rule.target = target
     rule.target = target
     if rule not in chain.rules:
     if rule not in chain.rules:
       chain.insert_rule(rule)
       chain.insert_rule(rule)
-  # Apply blacklist
-  BLACKLIST = r.hgetall('F2B_BLACKLIST')
-  if BLACKLIST:
-    for bl_key in BLACKLIST:
-      if type(ipaddress.ip_network(bl_key.decode('ascii'), strict=False)) is ipaddress.IPv4Network:
-        chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
-        rule = iptc.Rule()
-        rule.src = bl_key
-        target = iptc.Target(rule, "REJECT")
-        rule.target = target
-        if rule not in chain.rules:
-          log['time'] = int(round(time.time()))
-          log['priority'] = 'crit'
-          log['message'] = 'Blacklisting host/network %s' % bl_key
-          r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-          print log['message']
-          chain.insert_rule(rule)
-          r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
-      else:
-        chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
-        rule = iptc.Rule6()
-        rule.src = bl_key
-        target = iptc.Target(rule, "REJECT")
-        rule.target = target
-        if rule not in chain.rules:
-          log['time'] = int(round(time.time()))
-          log['priority'] = 'crit'
-          log['message'] = 'Blacklisting host/network %s' % bl_key
-          r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
-          print log['message']
-          chain.insert_rule(rule)
-          r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
+ 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
 
 
@@ -437,25 +514,25 @@ if __name__ == '__main__':
 
 
   if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n':
   if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n':
     try:
     try:
-      snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
+      snat_ip = os.getenv('SNAT_TO_SOURCE')
       snat_ipo = ipaddress.ip_address(snat_ip)
       snat_ipo = ipaddress.ip_address(snat_ip)
       if type(snat_ipo) is ipaddress.IPv4Address:
       if type(snat_ipo) is ipaddress.IPv4Address:
         snat4_thread = Thread(target=snat4,args=(snat_ip,))
         snat4_thread = Thread(target=snat4,args=(snat_ip,))
         snat4_thread.daemon = True
         snat4_thread.daemon = True
         snat4_thread.start()
         snat4_thread.start()
     except ValueError:
     except ValueError:
-      print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
+      print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
 
 
   if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') is not 'n':
   if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') is not 'n':
     try:
     try:
-      snat_ip = os.getenv('SNAT6_TO_SOURCE').decode('ascii')
+      snat_ip = os.getenv('SNAT6_TO_SOURCE')
       snat_ipo = ipaddress.ip_address(snat_ip)
       snat_ipo = ipaddress.ip_address(snat_ip)
       if type(snat_ipo) is ipaddress.IPv6Address:
       if type(snat_ipo) is ipaddress.IPv6Address:
         snat6_thread = Thread(target=snat6,args=(snat_ip,))
         snat6_thread = Thread(target=snat6,args=(snat_ip,))
         snat6_thread.daemon = True
         snat6_thread.daemon = True
         snat6_thread.start()
         snat6_thread.start()
     except ValueError:
     except ValueError:
-      print os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address'
+      print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
 
 
   autopurge_thread = Thread(target=autopurge)
   autopurge_thread = Thread(target=autopurge)
   autopurge_thread.daemon = True
   autopurge_thread.daemon = True
@@ -464,6 +541,14 @@ if __name__ == '__main__':
   mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
   mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
   mailcowchainwatch_thread.daemon = True
   mailcowchainwatch_thread.daemon = True
   mailcowchainwatch_thread.start()
   mailcowchainwatch_thread.start()
+  
+  blacklistupdate_thread = Thread(target=blacklistUpdate)
+  blacklistupdate_thread.daemon = True
+  blacklistupdate_thread.start()
+
+  whitelistupdate_thread = Thread(target=whitelistUpdate)
+  whitelistupdate_thread.daemon = True
+  whitelistupdate_thread.start()
 
 
   signal.signal(signal.SIGTERM, quit)
   signal.signal(signal.SIGTERM, quit)
   atexit.register(clear)
   atexit.register(clear)

+ 19 - 0
data/Dockerfiles/olefy/Dockerfile

@@ -0,0 +1,19 @@
+FROM alpine:3.10
+LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
+
+WORKDIR /app
+
+#RUN addgroup -S olefy && adduser -S olefy -G olefy \
+RUN apk add --virtual .build-deps gcc python3-dev musl-dev libffi-dev openssl-dev \
+  && apk add --update --no-cache python3 openssl tzdata libmagic \
+  && pip3 install --upgrade pip \
+  && pip3 install --upgrade oletools asyncio python-magic \
+  && apk del .build-deps
+
+ADD https://raw.githubusercontent.com/HeinleinSupport/olefy/master/olefy.py /app/
+
+RUN chown -R nobody:nobody /app /tmp
+
+USER nobody
+
+CMD ["python3", "-u", "/app/olefy.py"]

+ 6 - 5
data/Dockerfiles/phpfpm/Dockerfile

@@ -1,11 +1,11 @@
-FROM php:7.3-fpm-alpine3.8
+FROM php:7.3-fpm-alpine3.10
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
-ENV APCU_PECL 5.1.16
-ENV IMAGICK_PECL 3.4.3
+ENV APCU_PECL 5.1.17
+ENV IMAGICK_PECL 3.4.4
 #ENV MAILPARSE_PECL 3.0.2
 #ENV MAILPARSE_PECL 3.0.2
 ENV MEMCACHED_PECL 3.1.3
 ENV MEMCACHED_PECL 3.1.3
-ENV REDIS_PECL 4.2.0
+ENV REDIS_PECL 5.0.1
 
 
 RUN apk add -U --no-cache autoconf \
 RUN apk add -U --no-cache autoconf \
   bash \
   bash \
@@ -53,13 +53,14 @@ RUN apk add -U --no-cache autoconf \
   && docker-php-ext-enable apcu imagick memcached mailparse redis \
   && docker-php-ext-enable apcu imagick memcached mailparse redis \
   && pecl clear-cache \
   && pecl clear-cache \
   && docker-php-ext-configure intl \
   && docker-php-ext-configure intl \
+  && docker-php-ext-configure exif \
   && docker-php-ext-configure gd \
   && docker-php-ext-configure gd \
     --with-gd \
     --with-gd \
     --enable-gd-native-ttf \
     --enable-gd-native-ttf \
     --with-freetype-dir=/usr/include/ \
     --with-freetype-dir=/usr/include/ \
     --with-png-dir=/usr/include/ \
     --with-png-dir=/usr/include/ \
     --with-jpeg-dir=/usr/include/ \
     --with-jpeg-dir=/usr/include/ \
-  && docker-php-ext-install -j 4 gd gettext intl ldap opcache pcntl pdo pdo_mysql soap sockets xmlrpc zip \
+  && docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql soap sockets xmlrpc zip \
   && docker-php-ext-configure imap --with-imap --with-imap-ssl \
   && docker-php-ext-configure imap --with-imap --with-imap-ssl \
   && docker-php-ext-install -j 4 imap \
   && docker-php-ext-install -j 4 imap \
   && apk del --purge autoconf \
   && apk del --purge autoconf \

+ 33 - 14
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -19,29 +19,48 @@ if [[ -z $(redis-cli --raw -h redis-mailcow GET Q_RELEASE_FORMAT) ]]; then
   redis-cli --raw -h redis-mailcow SET Q_RELEASE_FORMAT raw
   redis-cli --raw -h redis-mailcow SET Q_RELEASE_FORMAT raw
 fi
 fi
 
 
+# Set max age of q items - if unset
+
+if [[ -z $(redis-cli --raw -h redis-mailcow GET Q_MAX_AGE) ]]; then
+  redis-cli --raw -h redis-mailcow SET Q_MAX_AGE 365
+fi
+
 # Check of mysql_upgrade
 # Check of mysql_upgrade
 
 
 CONTAINER_ID=
 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
+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"
+    POSTFIX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("postfix-mailcow")) | .id' | tr "\n" " "))
+    if [[ -z ${POSTFIX} ]]; then
+      echo "Could not determine Postfix container ID, skipping Postfix restart."
     else
     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
+      echo "Restarting Postfix"
+      curl -X POST --silent --insecure https://dockerapi/containers/${POSTFIX}/restart | jq -r '.msg'
+      echo "Sleeping 10 seconds..."
+      sleep 10
     fi
     fi
+    echo "Restarting PHP-FPM, bye"
+    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

+ 8 - 12
data/Dockerfiles/postfix/Dockerfile

@@ -1,4 +1,4 @@
-FROM ubuntu:bionic
+FROM debian:buster-slim
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
 ARG DEBIAN_FRONTEND=noninteractive
 ARG DEBIAN_FRONTEND=noninteractive
@@ -9,12 +9,17 @@ RUN dpkg-divert --local --rename --add /sbin/initctl \
 	&& dpkg-divert --local --rename --add /usr/bin/ischroot \
 	&& dpkg-divert --local --rename --add /usr/bin/ischroot \
 	&& ln -sf /bin/true /usr/bin/ischroot
 	&& ln -sf /bin/true /usr/bin/ischroot
 
 
-RUN apt-get update && apt-get install -y --no-install-recommends \
+# Add groups and users before installing Postfix to not break compatibility
+RUN groupadd -g 102 postfix \
+  && groupadd -g 103 postdrop \
+  && useradd -g postfix -u 101 -d /var/spool/postfix -s /usr/sbin/nologin postfix \
+  && apt-get update && apt-get install -y --no-install-recommends \
 	ca-certificates \
 	ca-certificates \
 	curl \
 	curl \
 	dirmngr \
 	dirmngr \
 	gnupg \
 	gnupg \
 	libsasl2-modules \
 	libsasl2-modules \
+  mariadb-client \
 	perl \
 	perl \
 	postfix \
 	postfix \
 	postfix-mysql \
 	postfix-mysql \
@@ -32,18 +37,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
   && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
   && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
   && chmod +x /usr/local/sbin/postconf
   && chmod +x /usr/local/sbin/postconf
 
 
-RUN addgroup --system --gid 600 zeyple \
-  && adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple \
-  && touch /var/log/zeyple.log \
-  && chown zeyple: /var/log/zeyple.log \
-  && mkdir -p /opt/mailman/var/data \
-  && touch /opt/mailman/var/data/postfix_lmtp \
-  && touch /opt/mailman/var/data/postfix_domains
-
-COPY zeyple.py /usr/local/bin/zeyple.py
-COPY zeyple.conf /etc/zeyple.conf
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
 COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
+COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
 COPY postfix.sh /opt/postfix.sh
 COPY postfix.sh /opt/postfix.sh
 COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
 COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
 COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
 COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam

+ 43 - 18
data/Dockerfiles/postfix/postfix.sh

@@ -4,14 +4,23 @@ trap "postfix stop" EXIT
 
 
 [[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
 [[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
 
 
+# Wait for MySQL to warm-up
+while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
+  echo "Waiting for database to come up..."
+  sleep 2
+done
+
 cat <<EOF > /etc/aliases
 cat <<EOF > /etc/aliases
+# Autogenerated by mailcow
 null: /dev/null
 null: /dev/null
+watchdog: /dev/null
 ham: "|/usr/local/bin/rspamd-pipe-ham"
 ham: "|/usr/local/bin/rspamd-pipe-ham"
 spam: "|/usr/local/bin/rspamd-pipe-spam"
 spam: "|/usr/local/bin/rspamd-pipe-spam"
 EOF
 EOF
 newaliases;
 newaliases;
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -30,6 +39,7 @@ query = SELECT DISTINCT
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -38,6 +48,7 @@ query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_ove
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -55,6 +66,7 @@ query = SELECT IF(EXISTS(
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -86,6 +98,7 @@ query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_transport_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_transport_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -95,7 +108,18 @@ query = SELECT CONCAT('smtp_via_transport_maps:', nexthop) AS transport FROM tra
   AND destination = '%s';
   AND destination = '%s';
 EOF
 EOF
 
 
+cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_resource_maps.cf
+# Autogenerated by mailcow
+user = ${DBUSER}
+password = ${DBPASS}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = ${DBNAME}
+query = SELECT 'null@localhost' FROM mailbox
+  WHERE kind REGEXP 'location|thing|group' AND username = '%s';
+EOF
+
 cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -104,8 +128,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'
@@ -113,6 +137,7 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -124,18 +149,8 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports
   LIMIT 1;
   LIMIT 1;
 EOF
 EOF
 
 
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT goto FROM alias, alias_domain
-  WHERE alias_domain.alias_domain = '%d'
-    AND alias.address = CONCAT('@', alias_domain.target_domain)
-    AND alias.active = 1 AND alias_domain.active='1'
-EOF
-
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -148,6 +163,7 @@ query = SELECT username FROM mailbox, alias_domain
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -158,6 +174,7 @@ query = SELECT goto FROM alias
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -169,6 +186,7 @@ query = SELECT bcc_dest FROM bcc_maps
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -180,6 +198,7 @@ query = SELECT bcc_dest FROM bcc_maps
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -190,6 +209,7 @@ query = SELECT new_dest FROM recipient_maps
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -203,6 +223,7 @@ query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -211,6 +232,7 @@ query = SELECT CONCAT(JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.mailbox_format'))
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -219,6 +241,7 @@ query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND activ
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -260,6 +283,7 @@ query = SELECT goto FROM alias
 EOF
 EOF
 
 
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf
 cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf
+# Autogenerated by mailcow
 user = ${DBUSER}
 user = ${DBUSER}
 password = ${DBPASS}
 password = ${DBPASS}
 hosts = unix:/var/run/mysqld/mysqld.sock
 hosts = unix:/var/run/mysqld/mysqld.sock
@@ -269,10 +293,11 @@ query = SELECT goto FROM spamalias
     AND validity >= UNIX_TIMESTAMP()
     AND validity >= UNIX_TIMESTAMP()
 EOF
 EOF
 
 
-# Reset GPG key permissions
-mkdir -p /var/lib/zeyple/keys
-chmod 700 /var/lib/zeyple/keys
-chown -R 600:600 /var/lib/zeyple/keys
+sed -i '/User overrides/q' /opt/postfix/conf/main.cf
+echo >> /opt/postfix/conf/main.cf
+if [ -f /opt/postfix/conf/extra.cf ]; then
+  cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
+fi
 
 
 # Fix Postfix permissions
 # Fix Postfix permissions
 chown -R root:postfix /opt/postfix/conf/sql/
 chown -R root:postfix /opt/postfix/conf/sql/
@@ -282,7 +307,7 @@ chgrp -R postdrop /var/spool/postfix/maildrop
 postfix set-permissions
 postfix set-permissions
 
 
 # Check Postfix configuration
 # Check Postfix configuration
-postconf -c /opt/postfix/conf
+postconf -c /opt/postfix/conf > /dev/null
 
 
 if [[ $? != 0 ]]; then
 if [[ $? != 0 ]]; then
   echo "Postfix configuration error, refusing to start."
   echo "Postfix configuration error, refusing to start."

+ 5 - 0
data/Dockerfiles/postfix/supervisord.conf

@@ -1,4 +1,5 @@
 [supervisord]
 [supervisord]
+pidfile=/var/run/supervisord.pid
 nodaemon=true
 nodaemon=true
 user=root
 user=root
 
 
@@ -12,6 +13,10 @@ autostart=true
 
 
 [program:postfix]
 [program:postfix]
 command=/opt/postfix.sh
 command=/opt/postfix.sh
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
 autorestart=true
 autorestart=true
 
 
 [eventlistener:processes]
 [eventlistener:processes]

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

@@ -1,9 +1,10 @@
-@version: 3.13
+@version: 3.19
 @include "scl.conf"
 @include "scl.conf"
 options {
 options {
   chain_hostnames(off);
   chain_hostnames(off);
   flush_lines(0);
   flush_lines(0);
   use_dns(no);
   use_dns(no);
+  dns_cache(no);
   use_fqdn(no);
   use_fqdn(no);
   owner("root"); group("adm"); perm(0640);
   owner("root"); group("adm"); perm(0640);
   stats_freq(0);
   stats_freq(0);

+ 0 - 9
data/Dockerfiles/postfix/zeyple.conf

@@ -1,9 +0,0 @@
-[zeyple]
-log_file = /dev/null
-
-[gpg]
-home = /var/lib/zeyple/keys
-
-[relay]
-host = localhost
-port = 10026

+ 0 - 274
data/Dockerfiles/postfix/zeyple.py

@@ -1,274 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import sys
-import os
-import logging
-import email
-import email.mime.multipart
-import email.mime.application
-import email.encoders
-import smtplib
-import copy
-from io import BytesIO
-
-try:
-    from configparser import SafeConfigParser  # Python 3
-except ImportError:
-    from ConfigParser import SafeConfigParser  # Python 2
-
-import gpgme
-
-# Boiler plate to avoid dependency on six
-# BBB: Python 2.7 support
-PY3K = sys.version_info > (3, 0)
-
-
-def message_from_binary(message):
-    if PY3K:
-        return email.message_from_bytes(message)
-    else:
-        return email.message_from_string(message)
-
-
-def as_binary_string(email):
-    if PY3K:
-        return email.as_bytes()
-    else:
-        return email.as_string()
-
-
-def encode_string(string):
-    if isinstance(string, bytes):
-        return string
-    else:
-        return string.encode('utf-8')
-
-
-__title__ = 'Zeyple'
-__version__ = '1.2.0'
-__author__ = 'Cédric Félizard'
-__license__ = 'AGPLv3+'
-__copyright__ = 'Copyright 2012-2016 Cédric Félizard'
-
-
-class Zeyple:
-    """Zeyple Encrypts Your Precious Log Emails"""
-
-    def __init__(self, config_fname='zeyple.conf'):
-        self.config = self.load_configuration(config_fname)
-
-        log_file = self.config.get('zeyple', 'log_file')
-        logging.basicConfig(
-            filename=log_file, level=logging.DEBUG,
-            format='%(asctime)s %(process)s %(levelname)s %(message)s'
-        )
-        logging.info("Zeyple ready to encrypt outgoing emails")
-
-    def load_configuration(self, filename):
-        """Reads and parses the config file"""
-
-        config = SafeConfigParser()
-        config.read([
-            os.path.join('/etc/', filename),
-            filename,
-        ])
-        if not config.sections():
-            raise IOError('Cannot open config file.')
-        return config
-
-    @property
-    def gpg(self):
-        protocol = gpgme.PROTOCOL_OpenPGP
-
-        if self.config.has_option('gpg', 'executable'):
-            executable = self.config.get('gpg', 'executable')
-        else:
-            executable = None  # Default value
-
-        home_dir = self.config.get('gpg', 'home')
-
-        ctx = gpgme.Context()
-        ctx.set_engine_info(protocol, executable, home_dir)
-        ctx.armor = True
-
-        return ctx
-
-    def process_message(self, message_data, recipients):
-        """Encrypts the message with recipient keys"""
-        message_data = encode_string(message_data)
-
-        in_message = message_from_binary(message_data)
-        logging.info(
-            "Processing outgoing message %s", in_message['Message-id'])
-
-        if not recipients:
-            logging.warn("Cannot find any recipients, ignoring")
-
-        sent_messages = []
-        for recipient in recipients:
-            logging.info("Recipient: %s", recipient)
-
-            key_id = self._user_key(recipient)
-            logging.info("Key ID: %s", key_id)
-
-            if key_id:
-                out_message = self._encrypt_message(in_message, key_id)
-
-                # Delete Content-Transfer-Encoding if present to default to
-                # "7bit" otherwise Thunderbird seems to hang in some cases.
-                del out_message["Content-Transfer-Encoding"]
-            else:
-                logging.warn("No keys found, message will be sent unencrypted")
-                out_message = copy.copy(in_message)
-
-            self._add_zeyple_header(out_message)
-            self._send_message(out_message, recipient)
-            sent_messages.append(out_message)
-
-        return sent_messages
-
-    def _get_version_part(self):
-        ret = email.mime.application.MIMEApplication(
-            'Version: 1\n',
-            'pgp-encrypted',
-            email.encoders.encode_noop,
-        )
-        ret.add_header(
-            'Content-Description',
-            "PGP/MIME version identification",
-        )
-        return ret
-
-    def _get_encrypted_part(self, payload):
-        ret = email.mime.application.MIMEApplication(
-            payload,
-            'octet-stream',
-            email.encoders.encode_noop,
-            name="encrypted.asc",
-        )
-        ret.add_header('Content-Description', "OpenPGP encrypted message")
-        ret.add_header(
-            'Content-Disposition',
-            'inline',
-            filename='encrypted.asc',
-        )
-        return ret
-
-    def _encrypt_message(self, in_message, key_id):
-        if in_message.is_multipart():
-            # get the body (after the first \n\n)
-            payload = in_message.as_string().split("\n\n", 1)[1].strip()
-
-            # prepend the Content-Type including the boundary
-            content_type = "Content-Type: " + in_message["Content-Type"]
-            payload = content_type + "\n\n" + payload
-
-            message = email.message.Message()
-            message.set_payload(payload)
-
-            payload = message.get_payload()
-
-        else:
-            payload = in_message.get_payload()
-            payload = encode_string(payload)
-
-            quoted_printable = email.charset.Charset('ascii')
-            quoted_printable.body_encoding = email.charset.QP
-
-            message = email.mime.nonmultipart.MIMENonMultipart(
-                'text', 'plain', charset='utf-8'
-            )
-            message.set_payload(payload, charset=quoted_printable)
-
-            mixed = email.mime.multipart.MIMEMultipart(
-                'mixed',
-                None,
-                [message],
-            )
-
-            # remove superfluous header
-            del mixed['MIME-Version']
-
-            payload = as_binary_string(mixed)
-
-        encrypted_payload = self._encrypt_payload(payload, [key_id])
-
-        version = self._get_version_part()
-        encrypted = self._get_encrypted_part(encrypted_payload)
-
-        out_message = copy.copy(in_message)
-        out_message.preamble = "This is an OpenPGP/MIME encrypted " \
-                               "message (RFC 4880 and 3156)"
-
-        if 'Content-Type' not in out_message:
-            out_message['Content-Type'] = 'multipart/encrypted'
-        else:
-            out_message.replace_header(
-                'Content-Type',
-                'multipart/encrypted',
-            )
-
-        out_message.set_param('protocol', 'application/pgp-encrypted')
-        out_message.set_payload([version, encrypted])
-
-        return out_message
-
-    def _encrypt_payload(self, payload, key_ids):
-        """Encrypts the payload with the given keys"""
-        payload = encode_string(payload)
-
-        plaintext = BytesIO(payload)
-        ciphertext = BytesIO()
-
-        self.gpg.armor = True
-
-        recipient = [self.gpg.get_key(key_id) for key_id in key_ids]
-
-        self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST,
-                         plaintext, ciphertext)
-
-        return ciphertext.getvalue()
-
-    def _user_key(self, email):
-        """Returns the GPG key for the given email address"""
-        logging.info("Trying to encrypt for %s", email)
-        keys = [key for key in self.gpg.keylist(email)]
-
-        if keys:
-            key = keys.pop()  # NOTE: looks like keys[0] is the master key
-            key_id = key.subkeys[0].keyid
-            return key_id
-
-        return None
-
-    def _add_zeyple_header(self, message):
-        if self.config.has_option('zeyple', 'add_header') and \
-           self.config.getboolean('zeyple', 'add_header'):
-            message.add_header(
-                'X-Zeyple',
-                "processed by {0} v{1}".format(__title__, __version__)
-            )
-
-    def _send_message(self, message, recipient):
-        """Sends the given message through the SMTP relay"""
-        logging.info("Sending message %s", message['Message-id'])
-
-        smtp = smtplib.SMTP(self.config.get('relay', 'host'),
-                            self.config.get('relay', 'port'))
-
-        smtp.sendmail(message['From'], recipient, message.as_string())
-        smtp.quit()
-
-        logging.info("Message %s sent", message['Message-id'])
-
-
-if __name__ == '__main__':
-    recipients = sys.argv[1:]
-
-    # BBB: Python 2.7 support
-    binary_stdin = sys.stdin.buffer if PY3K else sys.stdin
-    message = binary_stdin.read()
-
-    zeyple = Zeyple()
-    zeyple.process_message(message, recipients)

+ 16 - 14
data/Dockerfiles/rspamd/Dockerfile

@@ -1,27 +1,29 @@
-FROM ubuntu:bionic
+FROM debian:buster-slim
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
 ARG DEBIAN_FRONTEND=noninteractive
 ARG DEBIAN_FRONTEND=noninteractive
+ARG CODENAME=buster
 ENV LC_ALL C
 ENV LC_ALL C
 
 
 RUN apt-get update && apt-get install -y \
 RUN apt-get update && apt-get install -y \
   tzdata \
   tzdata \
-	ca-certificates \
-	gnupg2 \
-	apt-transport-https \
-	&& apt-key adv --fetch-keys https://rspamd.com/apt/gpg.key \
-	&& echo "deb https://rspamd.com/apt-stable/ bionic main" > /etc/apt/sources.list.d/rspamd.list \
-	&& apt-get update && apt-get install -y rspamd \
-	&& 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 clean \
-	&& mkdir -p /run/rspamd \
-	&& chown _rspamd:_rspamd /run/rspamd
+  ca-certificates \
+  gnupg2 \
+  apt-transport-https \
+  dnsutils \
+  && apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
+  && echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
+  && echo "deb-src [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" >> /etc/apt/sources.list.d/rspamd.list \
+  && apt-get update \
+  && apt-get --no-install-recommends -y install rspamd \
+  && rm -rf /var/lib/apt/lists/* \
+  && apt-get autoremove --purge \
+  && apt-get clean \
+  && mkdir -p /run/rspamd \
+  && chown _rspamd:_rspamd /run/rspamd
 
 
 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"]
 
 

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

@@ -1,9 +1,37 @@
 #!/bin/bash
 #!/bin/bash
 
 
-chown -R _rspamd:_rspamd /var/lib/rspamd /etc/rspamd/local.d /etc/rspamd/override.d /etc/rspamd/custom
+mkdir -p /etc/rspamd/plugins.d \
+  /etc/rspamd/custom
+
+touch /etc/rspamd/rspamd.conf.local \
+  /etc/rspamd/rspamd.conf.override
+
 chmod 755 /var/lib/rspamd
 chmod 755 /var/lib/rspamd
-[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Placeholder' > /etc/rspamd/override.d/worker-controller-password.inc
-chown _rspamd:_rspamd /etc/rspamd/override.d/worker-controller-password.inc
-[[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]] && echo '# to be auto-filled by dovecot-mailcow' > /etc/rspamd/custom/sa-rules-heinlein
+
+[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc
+[[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/custom/sa-rules-heinlein
+[[ ! -f /etc/rspamd/custom/dovecot_trusted.map ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/custom/dovecot_trusted.map
+
+DOVECOT_V4=
+DOVECOT_V6=
+until [[ ! -z ${DOVECOT_V4} ]]; do
+  DOVECOT_V4=$(dig a dovecot +short)
+  DOVECOT_V6=$(dig aaaa dovecot +short)
+  [[ ! -z ${DOVECOT_V4} ]] && break;
+  echo "Waiting for Dovecot"
+  sleep 3
+done
+echo ${DOVECOT_V4}/32 > /etc/rspamd/custom/dovecot_trusted.map
+if [[ ! -z ${DOVECOT_V6} ]]; then
+  echo ${DOVECOT_V6}/128 >> /etc/rspamd/custom/dovecot_trusted.map
+fi
+
+chown -R _rspamd:_rspamd /var/lib/rspamd \
+  /etc/rspamd/local.d \
+  /etc/rspamd/override.d \
+  /etc/rspamd/custom \
+  /etc/rspamd/rspamd.conf.local \
+  /etc/rspamd/rspamd.conf.override \
+  /etc/rspamd/plugins.d
 
 
 exec "$@"
 exec "$@"

+ 6 - 13
data/Dockerfiles/sogo/Dockerfile

@@ -3,7 +3,7 @@ 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 GOSU_VERSION 1.9
+ENV GOSU_VERSION 1.11
 
 
 # Prerequisites
 # Prerequisites
 RUN apt-get update && apt-get install -y --no-install-recommends \
 RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -13,6 +13,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
   gettext \
   gettext \
   gnupg \
   gnupg \
   mysql-client \
   mysql-client \
+  rsync \
   supervisor \
   supervisor \
   syslog-ng \
   syslog-ng \
   syslog-ng-core \
   syslog-ng-core \
@@ -22,23 +23,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
   psmisc \
   psmisc \
   wget \
   wget \
   patch \
   patch \
-  && rm -rf /var/lib/apt/lists/* \
   && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
   && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
   && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
   && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
   && chmod +x /usr/local/bin/gosu \
   && chmod +x /usr/local/bin/gosu \
-  && gosu nobody true
-
-RUN mkdir /usr/share/doc/sogo \
+  && gosu nobody true \
+  && mkdir /usr/share/doc/sogo \
   && touch /usr/share/doc/sogo/empty.sh \
   && touch /usr/share/doc/sogo/empty.sh \
   && apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \
   && apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \
   && echo "deb http://packages.inverse.ca/SOGo/nightly/4/debian/ stretch stretch" > /etc/apt/sources.list.d/sogo.list \
   && echo "deb http://packages.inverse.ca/SOGo/nightly/4/debian/ stretch stretch" > /etc/apt/sources.list.d/sogo.list \
-  && apt-get update && apt-get install -y --force-yes \
+  && apt-get update && apt-get install -y --no-install-recommends \
     sogo \
     sogo \
     sogo-activesync \
     sogo-activesync \
+  && apt-get autoclean \
   && rm -rf /var/lib/apt/lists/* \
   && rm -rf /var/lib/apt/lists/* \
-  && echo '* * * * *   sogo   /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds 2>/dev/null' > /etc/cron.d/sogo \
-  && echo '* * * * *   sogo   /usr/sbin/sogo-tool expire-sessions 60' >> /etc/cron.d/sogo \
-  && echo '0 0 * * *   sogo   /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds' >> /etc/cron.d/sogo \
   && touch /etc/default/locale
   && touch /etc/default/locale
 
 
 COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
 COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
@@ -51,7 +48,3 @@ RUN chmod +x /bootstrap-sogo.sh \
   /usr/local/sbin/stop-supervisor.sh
   /usr/local/sbin/stop-supervisor.sh
 
 
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
-
-VOLUME /usr/lib/GNUstep/SOGo/
-
-RUN rm -rf /tmp/* /var/tmp/*

+ 42 - 17
data/Dockerfiles/sogo/bootstrap-sogo.sh

@@ -14,11 +14,11 @@ do
 done
 done
 
 
 # Wait for updated schema
 # Wait for updated schema
-DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions;" -BN)
+DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
 DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
 DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
 while [[ ${DBV_NOW} != ${DBV_NEW} ]]; do
 while [[ ${DBV_NOW} != ${DBV_NEW} ]]; do
   echo "Waiting for schema update..."
   echo "Waiting for schema update..."
-  DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions;" -BN)
+  DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
   DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
   DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
   sleep 5
   sleep 5
 done
 done
@@ -30,10 +30,11 @@ mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e
 
 
 while [[ ${VIEW_OK} != 'OK' ]]; do
 while [[ ${VIEW_OK} != 'OK' ]]; do
   mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
   mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, kind, multiple_bookings) AS
-SELECT mailbox.username, mailbox.domain, mailbox.username, if(json_extract(attributes, '$.force_pw_update') LIKE '%0%', if(json_extract(attributes, '$.sogo_access') LIKE '%1%', password, 'invalid'), 'invalid'), mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gda.ad_alias, ''), mailbox.kind, mailbox.multiple_bookings FROM mailbox
+CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) AS
+SELECT mailbox.username, mailbox.domain, mailbox.username, if(json_extract(attributes, '$.force_pw_update') LIKE '%0%', if(json_extract(attributes, '$.sogo_access') LIKE '%1%', password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gda.ad_alias, ''), IFNULL(external_acl.send_as_acl, ''), mailbox.kind, mailbox.multiple_bookings FROM mailbox
 LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
 LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
 LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
 LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
+LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
 WHERE mailbox.active = '1'
 WHERE mailbox.active = '1'
 GROUP BY mailbox.username;
 GROUP BY mailbox.username;
 EOF
 EOF
@@ -51,7 +52,7 @@ while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
   if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
   if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
     STATIC_VIEW_OK=OK
     STATIC_VIEW_OK=OK
     echo "Updating _sogo_static_view content..."
     echo "Updating _sogo_static_view content..."
-    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, kind, multiple_bookings from sogo_view;"
+    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings from sogo_view;"
     mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
     mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
   else
   else
     echo "Waiting for database initialization..."
     echo "Waiting for database initialization..."
@@ -83,9 +84,16 @@ EOF
 done
 done
 
 
 
 
-mkdir -p /var/lib/sogo/GNUstep/Defaults/
+if [[ "${ALLOW_ADMIN_EMAIL_LOGIN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  TRUST_PROXY="YES"
+else
+  TRUST_PROXY="NO"
+fi
+# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
+RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
 
 
 # Generate plist header with timezone data
 # Generate plist header with timezone data
+mkdir -p /var/lib/sogo/GNUstep/Defaults/
 cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
 cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//GNUstep//DTD plist 0.9//EN" "http://www.gnustep.org/plist-0_9.xml">
 <!DOCTYPE plist PUBLIC "-//GNUstep//DTD plist 0.9//EN" "http://www.gnustep.org/plist-0_9.xml">
@@ -93,6 +101,12 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
 <dict>
 <dict>
     <key>OCSAclURL</key>
     <key>OCSAclURL</key>
     <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
     <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
+    <key>SOGoIMAPServer</key>
+    <string>imaps://${IPV4_NETWORK}.250:993</string>
+    <key>SOGoTrustProxyAuthentication</key>
+    <string>${TRUST_PROXY}</string>
+    <key>SOGoEncryptionKey</key>
+    <string>${RAND_PASS}</string>
     <key>OCSCacheFolderURL</key>
     <key>OCSCacheFolderURL</key>
     <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
     <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
     <key>OCSEMailAlarmsFolderURL</key>
     <key>OCSEMailAlarmsFolderURL</key>
@@ -125,6 +139,7 @@ while read -r line gal
                     <array>
                     <array>
                         <string>aliases</string>
                         <string>aliases</string>
                         <string>ad_aliases</string>
                         <string>ad_aliases</string>
+                        <string>ext_acl</string>
                     </array>
                     </array>
                     <key>KindFieldName</key>
                     <key>KindFieldName</key>
                     <string>kind</string>
                     <string>kind</string>
@@ -168,19 +183,29 @@ chown sogo:sogo -R /var/lib/sogo/
 chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
 chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
 
 
 # Patch ACLs
 # Patch ACLs
-if [[ ${ACL_ANYONE} == 'allow' ]]; then
-  #enable any or authenticated targets for ACL
-  if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
-    patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
-  fi
-else
-  #disable any or authenticated targets for ACL
-  if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
-    patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
-  fi
-fi
+#if [[ ${ACL_ANYONE} == 'allow' ]]; then
+#  #enable any or authenticated targets for ACL
+#  if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
+#    patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
+#  fi
+#else
+#  #disable any or authenticated targets for ACL
+#  if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
+#    patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
+#  fi
+#fi
 
 
 # Copy logo, if any
 # Copy logo, if any
 [[ -f /etc/sogo/sogo-full.svg ]] && cp /etc/sogo/sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg
 [[ -f /etc/sogo/sogo-full.svg ]] && cp /etc/sogo/sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg
 
 
+# Rsync web content
+echo "Syncing web content with named volume"
+rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
+
+# Creating cronjobs
+echo "* * * * *   sogo   /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds 2>/dev/null" > /etc/cron.d/sogo
+echo "* * * * *   sogo   /usr/sbin/sogo-tool expire-sessions ${SOGO_EXPIRE_SESSION}" >> /etc/cron.d/sogo
+echo "0 0 * * *   sogo   /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds" >> /etc/cron.d/sogo
+
+
 exec gosu sogo /usr/sbin/sogod
 exec gosu sogo /usr/sbin/sogod

+ 19 - 3
data/Dockerfiles/solr/Dockerfile

@@ -1,9 +1,25 @@
-FROM solr:7-alpine
+FROM solr:7.7-slim
+
 USER root
 USER root
+
+ENV GOSU_VERSION 1.11
+
 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 dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
+  && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
+  && chmod +x /usr/local/bin/gosu \
+  && gosu nobody true \
+  && apt-get update && apt-get install -y --no-install-recommends \
+  tzdata \
+  curl \
+  bash \
+  && apt-get autoclean \
+  && rm -rf /var/lib/apt/lists/* \
   && chmod +x /docker-entrypoint.sh \
   && chmod +x /docker-entrypoint.sh \
-  && /docker-entrypoint.sh --bootstrap
+  && sync \
+  && 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"
+  gosu 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\""
+  gosu 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"
+  gosu solr stop-local-solr
 
 
-if [[ "${1}" == "--bootstrap" ]]; then
   exit 0
   exit 0
-else
-  exec su-exec solr solr-foreground
 fi
 fi
+
+exec gosu 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>

+ 1 - 1
data/Dockerfiles/unbound/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.9
+FROM alpine:3.10
 
 
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 

+ 6 - 2
data/Dockerfiles/watchdog/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.9
+FROM alpine:3.10
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 
 
 # Installation
 # Installation
@@ -7,11 +7,13 @@ RUN apk add --update \
   nagios-plugins-tcp \
   nagios-plugins-tcp \
   nagios-plugins-http \
   nagios-plugins-http \
   nagios-plugins-ping \
   nagios-plugins-ping \
+  mariadb-client \
   curl \
   curl \
   bash \
   bash \
   coreutils \
   coreutils \
   jq \
   jq \
   fcgi \
   fcgi \
+  openssl \
   nagios-plugins-mysql \
   nagios-plugins-mysql \
   nagios-plugins-dns \
   nagios-plugins-dns \
   nagios-plugins-disk \
   nagios-plugins-disk \
@@ -26,11 +28,13 @@ RUN apk add --update \
   perl-term-readkey \
   perl-term-readkey \
   tini \
   tini \
   tzdata \
   tzdata \
+  whois \
   && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.9/smtp-cli -o /smtp-cli \
   && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.9/smtp-cli -o /smtp-cli \
   && chmod +x smtp-cli
   && chmod +x smtp-cli
 
 
 COPY watchdog.sh /watchdog.sh
 COPY watchdog.sh /watchdog.sh
 
 
-ENTRYPOINT ["/sbin/tini", "-g", "--"]
+#ENTRYPOINT ["/sbin/tini", "-g", "--"]
 # Less verbose
 # Less verbose
+
 CMD /watchdog.sh 2> /dev/null
 CMD /watchdog.sh 2> /dev/null

+ 305 - 58
data/Dockerfiles/watchdog/watchdog.sh

@@ -5,6 +5,8 @@ trap "kill 0" EXIT
 
 
 # Prepare
 # Prepare
 BACKGROUND_TASKS=()
 BACKGROUND_TASKS=()
+echo "Waiting for containers to settle..."
+sleep 10
 
 
 if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then
 if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then
   echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..."
   echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..."
@@ -17,7 +19,28 @@ if [[ ! -p /tmp/com_pipe ]]; then
   mkfifo /tmp/com_pipe
   mkfifo /tmp/com_pipe
 fi
 fi
 
 
+# Wait for containers
+while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
+  echo "Waiting for SQL..."
+  sleep 2
+done
+
+until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do
+  echo "Waiting for Redis..."
+  sleep 2
+done
+
+redis-cli -h redis-mailcow DEL F2B_RES > /dev/null
+
 # Common functions
 # Common functions
+array_diff() {
+  # https://stackoverflow.com/questions/2312762, Alex Offshore
+  eval local ARR1=\(\"\${$2[@]}\"\)
+  eval local ARR2=\(\"\${$3[@]}\"\)
+  local IFS=$'\n'
+  mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort))
+}
+
 progress() {
 progress() {
   SERVICE=${1}
   SERVICE=${1}
   TOTAL=${2}
   TOTAL=${2}
@@ -37,7 +60,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}")
 }
 }
@@ -46,6 +69,13 @@ function mail_error() {
   [[ -z ${1} ]] && return 1
   [[ -z ${1} ]] && return 1
   [[ -z ${2} ]] && BODY="Service was restarted on $(date), please check your mailcow installation." || BODY="$(date) - ${2}"
   [[ -z ${2} ]] && BODY="Service was restarted on $(date), please check your mailcow installation." || BODY="$(date) - ${2}"
   WATCHDOG_NOTIFY_EMAIL=$(echo "${WATCHDOG_NOTIFY_EMAIL}" | sed 's/"//;s|"$||')
   WATCHDOG_NOTIFY_EMAIL=$(echo "${WATCHDOG_NOTIFY_EMAIL}" | sed 's/"//;s|"$||')
+  # Some exceptions for subject and body formats
+  if [[ ${1} == "fail2ban" ]]; then
+    SUBJECT="${BODY}"
+    BODY="Please see netfilter-mailcow for more details and triggered rules."
+  else
+    SUBJECT="Watchdog ALERT: ${1}"
+  fi
   IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
   IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
   for rcpt in "${MAIL_RCPTS[@]}"; do
   for rcpt in "${MAIL_RCPTS[@]}"; do
     RCPT_DOMAIN=
     RCPT_DOMAIN=
@@ -56,15 +86,15 @@ function mail_error() {
       log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
       log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
       return 1
       return 1
     fi
     fi
-    [ -f "/tmp/${1}" ] && ATTACH="--attach /tmp/${1}@text/plain" || ATTACH=
-    ./smtp-cli --missing-modules-ok \
-      --subject="Watchdog: ${1} hit the error rate limit" \
+    [ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
+    timeout 10s ./smtp-cli --missing-modules-ok \
+      --charset=UTF-8 \
+      --subject="${SUBJECT}" \
       --body-plain="${BODY}" \
       --body-plain="${BODY}" \
       --to=${rcpt} \
       --to=${rcpt} \
       --from="watchdog@${MAILCOW_HOSTNAME}" \
       --from="watchdog@${MAILCOW_HOSTNAME}" \
       --server="${RCPT_MX}" \
       --server="${RCPT_MX}" \
-      --hello-host=${MAILCOW_HOSTNAME} \
-      ${ATTACH}
+      --hello-host=${MAILCOW_HOSTNAME}
     log_msg "Sent notification email to ${rcpt}"
     log_msg "Sent notification email to ${rcpt}"
   done
   done
 }
 }
@@ -111,11 +141,11 @@ get_container_ip() {
 nginx_checks() {
 nginx_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
-  THRESHOLD=16
+  THRESHOLD=5
   # 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} + $? ))
@@ -127,7 +157,7 @@ nginx_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -136,11 +166,11 @@ nginx_checks() {
 unbound_checks() {
 unbound_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
-  THRESHOLD=8
+  THRESHOLD=5
   # 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} + $? ))
@@ -159,7 +189,32 @@ unbound_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
+    fi
+  done
+  return 1
+}
+
+redis_checks() {
+  err_count=0
+  diff_c=0
+  THRESHOLD=5
+  # Reduce error count by 2 after restarting an unhealthy container
+  trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
+  while [ ${err_count} -lt ${THRESHOLD} ]; do
+    touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow
+    host_ip=$(get_container_ip redis-mailcow)
+    err_c_cur=${err_count}
+    /usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "PING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
+    [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
+    [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
+    progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
+    if [[ $? == 10 ]]; then
+      diff_c=0
+      sleep 1
+    else
+      diff_c=0
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -168,11 +223,11 @@ unbound_checks() {
 mysql_checks() {
 mysql_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
-  THRESHOLD=12
+  THRESHOLD=5
   # 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} + $? ))
@@ -185,7 +240,7 @@ mysql_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -194,11 +249,11 @@ mysql_checks() {
 sogo_checks() {
 sogo_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
-  THRESHOLD=10
+  THRESHOLD=5
   # 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} + $? ))
@@ -210,7 +265,7 @@ sogo_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -223,10 +278,10 @@ 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:watchdog@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 -S 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -S 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
     [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
     [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
     [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
     [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
@@ -236,7 +291,7 @@ postfix_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -245,11 +300,11 @@ postfix_checks() {
 clamd_checks() {
 clamd_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
-  THRESHOLD=5
+  THRESHOLD=15
   # 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} + $? ))
@@ -261,7 +316,7 @@ clamd_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 120 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -270,11 +325,11 @@ clamd_checks() {
 dovecot_checks() {
 dovecot_checks() {
   err_count=0
   err_count=0
   diff_c=0
   diff_c=0
-  THRESHOLD=20
+  THRESHOLD=12
   # 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} + $? ))
@@ -290,7 +345,7 @@ dovecot_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -303,7 +358,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} + $? ))
@@ -316,7 +371,7 @@ phpfpm_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -344,7 +399,73 @@ ratelimit_checks() {
       sleep 1
       sleep 1
     else
     else
       diff_c=0
       diff_c=0
-      sleep $(( ( RANDOM % 30 )  + 10 ))
+      sleep $(( ( RANDOM % 60 ) + 20 ))
+    fi
+  done
+  return 1
+}
+
+fail2ban_checks() {
+  err_count=0
+  diff_c=0
+  THRESHOLD=1
+  F2B_LOG_STATUS=($(redis-cli -h redis-mailcow --raw HKEYS F2B_ACTIVE_BANS))
+  F2B_RES=
+  # Reduce error count by 2 after restarting an unhealthy container
+  trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
+  while [ ${err_count} -lt ${THRESHOLD} ]; do
+    err_c_cur=${err_count}
+    F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]})
+    F2B_LOG_STATUS=($(redis-cli -h redis-mailcow --raw HKEYS F2B_ACTIVE_BANS))
+    array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV
+    if [[ ! -z "${F2B_RES}" ]]; then
+      err_count=$(( ${err_count} + 1 ))
+      echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s redis-cli -x -h redis-mailcow SET F2B_RES > /dev/null
+      if [ $? -ne 0 ]; then
+         redis-cli -x -h redis-mailcow DEL F2B_RES
+      fi
+    fi
+    [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
+    [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
+    progress "Fail2ban" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
+    if [[ $? == 10 ]]; then
+      diff_c=0
+      sleep 1
+    else
+      diff_c=0
+      sleep $(( ( RANDOM % 60 ) + 20 ))
+    fi
+  done
+  return 1
+}
+
+acme_checks() {
+  err_count=0
+  diff_c=0
+  THRESHOLD=1
+  ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME)
+  if [[ -z "${ACME_LOG_STATUS}" ]]; then
+    redis-cli -h redis SET ACME_FAIL_TIME 0
+    ACME_LOG_STATUS=0
+  fi
+  # Reduce error count by 2 after restarting an unhealthy container
+  trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
+  while [ ${err_count} -lt ${THRESHOLD} ]; do
+    err_c_cur=${err_count}
+    ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS}
+    ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME)
+    if [[ ${ACME_LOG_STATUS_PREV} != ${ACME_LOG_STATUS} ]]; then
+      err_count=$(( ${err_count} + 1 ))
+    fi
+    [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
+    [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
+    progress "ACME" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
+    if [[ $? == 10 ]]; then
+      diff_c=0
+      sleep 1
+    else
+      diff_c=0
+      sleep $(( ( RANDOM % 60 ) + 20 ))
     fi
     fi
   done
   done
   return 1
   return 1
@@ -358,10 +479,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 ))
@@ -372,15 +494,16 @@ ipv6nat_checks() {
     progress "IPv6 NAT" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
     progress "IPv6 NAT" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
     if [[ $? == 10 ]]; then
     if [[ $? == 10 ]]; then
       diff_c=0
       diff_c=0
-      sleep 1
+      sleep 30
     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 +511,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))
@@ -406,13 +528,49 @@ Empty
     [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
     [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
     [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
     [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
     progress "Rspamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
     progress "Rspamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
-    diff_c=0
-    sleep $(( ( RANDOM % 30 )  + 10 ))
+    if [[ $? == 10 ]]; then
+      diff_c=0
+      sleep 1
+    else
+      diff_c=0
+      sleep $(( ( RANDOM % 60 ) + 20 ))
+    fi
   done
   done
   return 1
   return 1
 }
 }
 
 
+olefy_checks() {
+  err_count=0
+  diff_c=0
+  THRESHOLD=5
+  # Reduce error count by 2 after restarting an unhealthy container
+  trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
+  while [ ${err_count} -lt ${THRESHOLD} ]; do
+    touch /tmp/olefy-mailcow; echo "$(tail -50 /tmp/olefy-mailcow)" > /tmp/olefy-mailcow
+    host_ip=$(get_container_ip olefy-mailcow)
+    err_c_cur=${err_count}
+    /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10055 2>> /tmp/olefy-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
+    [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
+    [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
+    progress "Olefy" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
+    if [[ $? == 10 ]]; then
+      diff_c=0
+      sleep 1
+    else
+      diff_c=0
+      sleep $(( ( RANDOM % 60 ) + 20 ))
+    fi
+  done
+  return 1
+}
+
+# Notify about start
+if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
+  mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
+fi
+
 # Create watchdog agents
 # Create watchdog agents
+
 (
 (
 while true; do
 while true; do
   if ! nginx_checks; then
   if ! nginx_checks; then
@@ -421,7 +579,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned nginx_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -431,7 +591,21 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned mysql_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
+
+(
+while true; do
+  if ! redis_checks; then
+    log_msg "Redis hit error limit"
+    echo redis-mailcow > /tmp/com_pipe
+  fi
+done
+) &
+PID=$!
+echo "Spawned redis_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -441,7 +615,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned phpfpm_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -451,7 +627,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned sogo_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 if [ ${CHECK_UNBOUND} -eq 1 ]; then
 if [ ${CHECK_UNBOUND} -eq 1 ]; then
 (
 (
@@ -462,7 +640,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned unbound_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 fi
 fi
 
 
 if [[ "${SKIP_CLAMD}" =~ ^([nN][oO]|[nN])+$ ]]; then
 if [[ "${SKIP_CLAMD}" =~ ^([nN][oO]|[nN])+$ ]]; then
@@ -474,7 +654,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned clamd_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 fi
 fi
 
 
 (
 (
@@ -485,7 +667,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned postfix_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -495,7 +679,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned dovecot_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -505,7 +691,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned rspamd_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -515,7 +703,45 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned ratelimit_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
+
+(
+while true; do
+  if ! fail2ban_checks; then
+    log_msg "Fail2ban hit error limit"
+    echo fail2ban > /tmp/com_pipe
+  fi
+done
+) &
+PID=$!
+echo "Spawned fail2ban_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
+
+#(
+#while true; do
+#  if ! olefy_checks; then
+#    log_msg "Olefy hit error limit"
+#    echo olefy-mailcow > /tmp/com_pipe
+#  fi
+#done
+#) &
+#PID=$!
+#echo "Spawned olefy_checks with PID ${PID}"
+#BACKGROUND_TASKS+=(${PID})
+
+(
+while true; do
+  if ! acme_checks; then
+    log_msg "ACME client hit error limit"
+    echo acme-mailcow > /tmp/com_pipe
+  fi
+done
+) &
+PID=$!
+echo "Spawned acme_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 (
 (
 while true; do
 while true; do
@@ -525,7 +751,9 @@ while true; do
   fi
   fi
 done
 done
 ) &
 ) &
-BACKGROUND_TASKS+=($!)
+PID=$!
+echo "Spawned ipv6nat_checks with PID ${PID}"
+BACKGROUND_TASKS+=(${PID})
 
 
 # Monitor watchdog agents, stop script when agents fails and wait for respawn by Docker (restart:always:n)
 # Monitor watchdog agents, stop script when agents fails and wait for respawn by Docker (restart:always:n)
 (
 (
@@ -556,25 +784,43 @@ while true; do
 done
 done
 ) &
 ) &
 
 
-# Restart container when threshold limit reached
+# Actions when threshold limit is reached
 while true; do
 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."
-  elif [[ ${com_pipe_answer} =~ .+-mailcow ]] || [[ ${com_pipe_answer} == "ipv6nat-mailcow" ]]; then
+    [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please see mailcow UI logs for further information."
+  elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
+    log_msg "acme-mailcow did not complete successfully"
+    [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
+  elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
+    F2B_RES=($(timeout 4s redis-cli -h redis-mailcow --raw GET F2B_RES 2> /dev/null))
+    if [[ ! -z "${F2B_RES}" ]]; then
+      redis-cli -h redis-mailcow DEL F2B_RES > /dev/null
+      host=
+      for host in "${F2B_RES[@]}"; do
+        log_msg "Banned ${host}"
+        rm /tmp/fail2ban 2> /dev/null
+        timeout 2s whois "${host}" > /tmp/fail2ban
+        [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}"
+      done
+    fi
+  elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
     kill -STOP ${BACKGROUND_TASKS[*]}
     kill -STOP ${BACKGROUND_TASKS[*]}
-    sleep 3
+    sleep 10
     CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id")
     CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id")
     if [[ ! -z ${CONTAINER_ID} ]]; then
     if [[ ! -z ${CONTAINER_ID} ]]; then
       if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
       if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
         HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
         HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
       fi
       fi
       S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
       S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
-      if [ ${S_RUNNING} -lt 120 ]; then
-        log_msg "Container is running for less than 120 seconds, skipping action..."
+      if [ ${S_RUNNING} -lt 360 ]; then
+        log_msg "Container is running for less than 360 seconds, skipping action..."
       elif [[ ! -z ${HAS_INITDB} ]]; then
       elif [[ ! -z ${HAS_INITDB} ]]; then
         log_msg "Database is being initialized by php-fpm-mailcow, not restarting but delaying checks for a minute..."
         log_msg "Database is being initialized by php-fpm-mailcow, not restarting but delaying checks for a minute..."
         sleep 60
         sleep 60
@@ -589,6 +835,7 @@ while true; do
       fi
       fi
     fi
     fi
     kill -CONT ${BACKGROUND_TASKS[*]}
     kill -CONT ${BACKGROUND_TASKS[*]}
+    sleep 1
     kill -USR1 ${BACKGROUND_TASKS[*]}
     kill -USR1 ${BACKGROUND_TASKS[*]}
   fi
   fi
 done
 done

+ 3 - 3
data/assets/nextcloud/nextcloud.conf

@@ -75,7 +75,7 @@ server {
     deny all;
     deny all;
   }
   }
 
 
-  location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
+  location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|oc[ms]-provider/.+)\.php(?:$|/) {
     fastcgi_split_path_info ^(.+\.php)(/.*)$;
     fastcgi_split_path_info ^(.+\.php)(/.*)$;
     include fastcgi_params;
     include fastcgi_params;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
@@ -90,12 +90,12 @@ server {
     fastcgi_read_timeout 1200;
     fastcgi_read_timeout 1200;
   }
   }
 
 
-  location ~ ^/(?:updater|ocs-provider)(?:$|/) {
+  location ~ ^/(?:updater|oc[ms]-provider)(?:$|/) {
     try_files $uri/ =404;
     try_files $uri/ =404;
     index index.php;
     index index.php;
   }
   }
 
 
-  location ~ \.(?:css|js|woff|svg|gif)$ {
+  location ~ \.(?:css|js|woff2?|svg|gif)$ {
     try_files $uri /index.php$uri$is_args$args;
     try_files $uri /index.php$uri$is_args$args;
     add_header Cache-Control "public, max-age=15778463";
     add_header Cache-Control "public, max-age=15778463";
     add_header X-Content-Type-Options nosniff;
     add_header X-Content-Type-Options nosniff;

+ 1 - 1
data/assets/nextcloud/occ

@@ -1,2 +1,2 @@
 #!/bin/bash
 #!/bin/bash
-docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ ${@}
+docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) php /web/nextcloud/occ ${@}

+ 0 - 44
data/assets/nextcloud/site.nextcloud.custom

@@ -1,44 +0,0 @@
-  location ^~ /nextcloud {
-    location /nextcloud {
-      rewrite ^ /nextcloud/index.php$uri;
-    }
-    location ~ ^/nextcloud/(?:build|tests|config|lib|3rdparty|templates|data)/ {
-      deny all;
-    }
-    location ~ ^/nextcloud/(?:\.|autotest|occ|issue|indie|db_|console) {
-      deny all;
-    }
-    location ~ ^/nextcloud/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {
-      fastcgi_split_path_info ^(.+\.php)(/.*)$;
-      include fastcgi_params;
-      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
-      fastcgi_param PATH_INFO $fastcgi_path_info;
-      fastcgi_param HTTPS on;
-      fastcgi_param modHeadersAvailable true;
-      fastcgi_param front_controller_active true;
-      fastcgi_pass phpfpm:9002;
-      fastcgi_intercept_errors on;
-      fastcgi_request_buffering off;
-      client_max_body_size 0;
-      fastcgi_read_timeout 1200;
-    }
-    location ~ ^/nextcloud/(?:updater|ocs-provider)(?:$|/) {
-      try_files $uri/ =404;
-      index index.php;
-    }
-    location ~ \.(?:css|js|woff|svg|gif)$ {
-      try_files $uri /nextcloud/index.php$uri$is_args$args;
-      add_header Cache-Control "public, max-age=15778463";
-      add_header X-Content-Type-Options nosniff;
-      add_header X-XSS-Protection "1; mode=block";
-      add_header X-Robots-Tag none;
-      add_header X-Download-Options noopen;
-      add_header X-Permitted-Cross-Domain-Policies none;
-      add_header X-Frame-Options "SAMEORIGIN";
-      access_log off;
-    }
-    location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
-      try_files $uri /nextcloud/index.php$uri$is_args$args;
-      access_log off;
-    }
-  }

+ 31 - 34
data/conf/dovecot/dovecot.conf

@@ -3,7 +3,7 @@
 # --------------------------------------------------------------------------
 # --------------------------------------------------------------------------
 # LDAP example:
 # LDAP example:
 #passdb {
 #passdb {
-#  args = /usr/local/etc/dovecot/ldap/passdb.conf
+#  args = /etc/dovecot/ldap/passdb.conf
 #  driver = ldap
 #  driver = ldap
 #}
 #}
 
 
@@ -20,7 +20,7 @@ disable_plaintext_auth = yes
 login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
 login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
 mail_home = /var/vmail/%d/%n
 mail_home = /var/vmail/%d/%n
 mail_location = maildir:~/
 mail_location = maildir:~/
-mail_plugins = </usr/local/etc/dovecot/mail_plugins
+mail_plugins = </etc/dovecot/mail_plugins
 mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
 mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
 mail_attachment_dir = /var/attachments
 mail_attachment_dir = /var/attachments
 mail_attachment_min_size = 128k
 mail_attachment_min_size = 128k
@@ -34,7 +34,7 @@ ssl_prefer_server_ciphers = yes
 ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
 ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
 
 
 # Default in Dovecot 2.3
 # Default in Dovecot 2.3
-ssl_options = no_compression
+ssl_options = no_compression no_ticket
 
 
 # New in Dovecot 2.3
 # New in Dovecot 2.3
 ssl_dh=</etc/ssl/mail/dhparams.pem
 ssl_dh=</etc/ssl/mail/dhparams.pem
@@ -47,12 +47,12 @@ mail_shared_explicit_inbox = yes
 mail_prefetch_count = 30
 mail_prefetch_count = 30
 passdb {
 passdb {
   driver = passwd-file
   driver = passwd-file
-  args = /usr/local/etc/dovecot/dovecot-master.passwd
+  args = /etc/dovecot/dovecot-master.passwd
   master = yes
   master = yes
   pass = yes
   pass = yes
 }
 }
 passdb {
 passdb {
-  args = /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf
+  args = /etc/dovecot/sql/dovecot-dict-sql-passdb.conf
   driver = sql
   driver = sql
   result_success = return-ok
   result_success = return-ok
   result_failure = continue
   result_failure = continue
@@ -60,7 +60,7 @@ passdb {
 }
 }
 passdb {
 passdb {
   driver = passwd-file
   driver = passwd-file
-  args = /usr/local/etc/dovecot/dovecot-master.passwd
+  args = /etc/dovecot/dovecot-master.passwd
   skip = authenticated
   skip = authenticated
 }
 }
 # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
 # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
@@ -206,14 +206,6 @@ namespace inbox {
   }
   }
   prefix =
   prefix =
 }
 }
-namespace {
-    type = shared
-    separator = /
-    prefix = Shared/%%u/
-    location = maildir:%%h/:INDEX=~/Shared/%%u;CONTROL=~/Shared/%%u
-    subscriptions = no
-    list = children
-}
 protocols = imap sieve lmtp pop3
 protocols = imap sieve lmtp pop3
 service dict {
 service dict {
   unix_listener dict {
   unix_listener dict {
@@ -282,52 +274,53 @@ ssl_cert = </etc/ssl/mail/cert.pem
 ssl_key = </etc/ssl/mail/key.pem
 ssl_key = </etc/ssl/mail/key.pem
 userdb {
 userdb {
   driver = passwd-file
   driver = passwd-file
-  args = /usr/local/etc/dovecot/dovecot-master.userdb
+  args = /etc/dovecot/dovecot-master.userdb
 }
 }
 userdb {
 userdb {
-  args = /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf
+  args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
   driver = sql
   driver = sql
   skip = found
   skip = found
 }
 }
 protocol imap {
 protocol imap {
-  mail_plugins = </usr/local/etc/dovecot/mail_plugins_imap
+  mail_plugins = </etc/dovecot/mail_plugins_imap
   imap_metadata = yes
   imap_metadata = yes
 }
 }
 mail_attribute_dict = file:%h/dovecot-attributes
 mail_attribute_dict = file:%h/dovecot-attributes
 protocol lmtp {
 protocol lmtp {
-  mail_plugins = </usr/local/etc/dovecot/mail_plugins_lmtp
-  auth_socket_path = /usr/local/var/run/dovecot/auth-master
+  mail_plugins = </etc/dovecot/mail_plugins_lmtp
+  auth_socket_path = /var/run/dovecot/auth-master
 }
 }
 protocol sieve {
 protocol sieve {
   managesieve_logout_format = bytes=%i/%o
   managesieve_logout_format = bytes=%i/%o
 }
 }
 plugin {
 plugin {
   # Allow "any" or "authenticated" to be used in ACLs
   # Allow "any" or "authenticated" to be used in ACLs
-  acl_anyone = </usr/local/etc/dovecot/acl_anyone
+  acl_anyone = </etc/dovecot/acl_anyone
   acl_shared_dict = file:/var/vmail/shared-mailboxes.db
   acl_shared_dict = file:/var/vmail/shared-mailboxes.db
   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
   sieve_plugins = sieve_imapsieve sieve_extprograms
   sieve_plugins = sieve_imapsieve sieve_extprograms
   sieve_vacation_send_from_recipient = yes
   sieve_vacation_send_from_recipient = yes
+  sieve_redirect_envelope_from = recipient
   # From elsewhere to Spam folder
   # From elsewhere to Spam folder
   imapsieve_mailbox1_name = Junk
   imapsieve_mailbox1_name = Junk
   imapsieve_mailbox1_causes = COPY
   imapsieve_mailbox1_causes = COPY
-  imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve
+  imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
   # END
   # END
   # From Spam folder to elsewhere
   # From Spam folder to elsewhere
   imapsieve_mailbox2_name = *
   imapsieve_mailbox2_name = *
   imapsieve_mailbox2_from = Junk
   imapsieve_mailbox2_from = Junk
   imapsieve_mailbox2_causes = COPY
   imapsieve_mailbox2_causes = COPY
-  imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve
+  imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
   # END
   # END
   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/lib/dovecot/sieve
   sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
   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
@@ -338,9 +331,10 @@ plugin {
   sieve_vacation_min_period = 5s
   sieve_vacation_min_period = 5s
   sieve_vacation_max_period = 0
   sieve_vacation_max_period = 0
   sieve_vacation_default_period = 60s
   sieve_vacation_default_period = 60s
-  sieve_before = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
+  sieve_before = /var/vmail/sieve/global_sieve_before.sieve
+  sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
   sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
   sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
-  sieve_after2 = /var/vmail/sieve/global.sieve
+  sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve
 
 
   # -- Global keys
   # -- Global keys
   mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
   mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
@@ -363,9 +357,9 @@ service quota-warning {
   }
   }
 }
 }
 dict {
 dict {
-  sqlquota = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf
-  sieve_after = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
-  sieve_before = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
+  sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf
+  sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
+  sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
 }
 }
 remote 127.0.0.1 {
 remote 127.0.0.1 {
   disable_plaintext_auth = no
   disable_plaintext_auth = no
@@ -384,9 +378,12 @@ 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
-!include_try /usr/local/etc/dovecot/extra.conf
+#auth_cache_verify_password_with_worker = yes
+#auth_cache_negative_ttl = 0
+#auth_cache_ttl = 30 s
+#auth_cache_size = 2 M
+!include_try /etc/dovecot/extra.conf
+!include_try /etc/dovecot/sogo-sso.conf
+!include_try /etc/dovecot/shared_namespace.conf
 default_client_limit = 10400
 default_client_limit = 10400
+default_vsz_limit = 1024 M

+ 3 - 0
data/conf/dovecot/sieve_after → data/conf/dovecot/global_sieve_after

@@ -1,3 +1,6 @@
+# global_sieve_after script
+# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
+
 require "fileinto";
 require "fileinto";
 require "mailbox";
 require "mailbox";
 require "variables";
 require "variables";

+ 2 - 0
data/conf/dovecot/global_sieve_before

@@ -0,0 +1,2 @@
+# global_sieve_before script
+# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after

+ 14 - 0
data/conf/nginx/site.conf

@@ -34,6 +34,7 @@ server {
 
 
   client_max_body_size 0;
   client_max_body_size 0;
 
 
+  listen 127.0.0.1:65510;
   include /etc/nginx/conf.d/listen_plain.active;
   include /etc/nginx/conf.d/listen_plain.active;
   include /etc/nginx/conf.d/listen_ssl.active;
   include /etc/nginx/conf.d/listen_ssl.active;
   include /etc/nginx/conf.d/server_name.active;
   include /etc/nginx/conf.d/server_name.active;
@@ -142,7 +143,19 @@ server {
     try_files /autoconfig.php =404;
     try_files /autoconfig.php =404;
   }
   }
 
 
+  # auth_request endpoint if ALLOW_ADMIN_EMAIL_LOGIN is set
+  location /sogo-auth-verify {
+    internal;
+    proxy_set_header  X-Original-URI $request_uri;
+    proxy_set_header  X-Real-IP $remote_addr;
+    proxy_set_header  Host $http_host;
+    proxy_set_header  Content-Length "";
+    proxy_pass        http://127.0.0.1:65510/sogo-auth;
+    proxy_pass_request_body off;
+  }
+
   location ^~ /Microsoft-Server-ActiveSync {
   location ^~ /Microsoft-Server-ActiveSync {
+    include /etc/nginx/conf.d/sogo_proxy_auth.active;
     include /etc/nginx/conf.d/sogo_eas.active;
     include /etc/nginx/conf.d/sogo_eas.active;
     proxy_connect_timeout 4000;
     proxy_connect_timeout 4000;
     proxy_next_upstream timeout error;
     proxy_next_upstream timeout error;
@@ -165,6 +178,7 @@ server {
   }
   }
 
 
   location ^~ /SOGo {
   location ^~ /SOGo {
+    include /etc/nginx/conf.d/sogo_proxy_auth.active;
     include /etc/nginx/conf.d/sogo.active;
     include /etc/nginx/conf.d/sogo.active;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

+ 10 - 0
data/conf/nginx/templates/sogo.auth_request.template.sh

@@ -0,0 +1,10 @@
+if printf "%s\n" "${ALLOW_ADMIN_EMAIL_LOGIN}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then
+    echo 'auth_request /sogo-auth-verify;
+auth_request_set $user $upstream_http_x_user;
+auth_request_set $auth $upstream_http_x_auth;
+auth_request_set $auth_type $upstream_http_x_auth_type;
+proxy_set_header x-webobjects-remote-user "$user";
+proxy_set_header Authorization "$auth";
+proxy_set_header x-webobjects-auth-type "$auth_type";
+'
+fi

+ 0 - 0
data/conf/phpfpm/sogo-sso/.gitkeep


+ 5 - 2
data/conf/postfix/anonymize_headers.pcre

@@ -1,8 +1,11 @@
 if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
 if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
-/^\s*Received:.*Authenticated sender:(.+)/
-  REPLACE Received: from localhost (localhost [127.0.0.1]) (Authenticated sender:$1
+#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/
+/^Received: from .*? \([\w-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
+  REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $2
 endif
 endif
 /^\s*X-Enigmail/        IGNORE
 /^\s*X-Enigmail/        IGNORE
 /^\s*X-Mailer/          IGNORE
 /^\s*X-Mailer/          IGNORE
 /^\s*X-Originating-IP/  IGNORE
 /^\s*X-Originating-IP/  IGNORE
 /^\s*X-Forward/         IGNORE
 /^\s*X-Forward/         IGNORE
+# Not removing UA by default, might be signed
+#/^\s*User-Agent/        IGNORE

+ 2 - 0
data/conf/postfix/local_transport

@@ -0,0 +1,2 @@
+/watchdog@localhost$/   watchdog_discard:
+/localhost$/  local:

+ 84 - 26
data/conf/postfix/main.cf

@@ -1,3 +1,6 @@
+# --------------------------------------------------------------------------
+# Please create a file "extra.cf" for persistent overrides to main.cf
+# --------------------------------------------------------------------------
 biff = no
 biff = no
 append_dot_mydomain = no
 append_dot_mydomain = no
 smtpd_tls_cert_file = /etc/ssl/mail/cert.pem
 smtpd_tls_cert_file = /etc/ssl/mail/cert.pem
@@ -6,7 +9,10 @@ smtpd_use_tls=yes
 smtpd_tls_received_header = yes
 smtpd_tls_received_header = yes
 smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
 smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
 smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
 smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
-smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
+smtpd_relay_restrictions = permit_mynetworks,
+  permit_sasl_authenticated,
+  defer_unauth_destination
+# alias maps are auto-generated in postfix.sh on startup
 alias_maps = hash:/etc/aliases
 alias_maps = hash:/etc/aliases
 alias_database = hash:/etc/aliases
 alias_database = hash:/etc/aliases
 relayhost =
 relayhost =
@@ -19,20 +25,53 @@ bounce_queue_lifetime = 1d
 broken_sasl_auth_clients = yes
 broken_sasl_auth_clients = yes
 disable_vrfy_command = yes
 disable_vrfy_command = yes
 maximal_backoff_time = 1800s
 maximal_backoff_time = 1800s
-maximal_queue_lifetime = 1d
+maximal_queue_lifetime = 5d
+delay_warning_time = 4h
 message_size_limit = 104857600
 message_size_limit = 104857600
 milter_default_action = accept
 milter_default_action = accept
 milter_protocol = 6
 milter_protocol = 6
 minimal_backoff_time = 300s
 minimal_backoff_time = 300s
 plaintext_reject_code = 550
 plaintext_reject_code = 550
-postscreen_access_list = permit_mynetworks, cidr:/opt/postfix/conf/postscreen_access.cidr, tcp:127.0.0.1:10027
+postscreen_access_list = permit_mynetworks,
+  cidr:/opt/postfix/conf/postscreen_access.cidr,
+  tcp:127.0.0.1:10027
 postscreen_bare_newline_enable = no
 postscreen_bare_newline_enable = no
 postscreen_blacklist_action = drop
 postscreen_blacklist_action = drop
 postscreen_cache_cleanup_interval = 24h
 postscreen_cache_cleanup_interval = 24h
 postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
 postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
 postscreen_dnsbl_action = enforce
 postscreen_dnsbl_action = enforce
-postscreen_dnsbl_sites = b.barracudacentral.org=127.0.0.2*7 dnsbl.inps.de=127.0.0.2*7 bl.mailspike.net=127.0.0.2*5 bl.mailspike.net=127.0.0.[10;11;12]*4 dnsbl.sorbs.net=127.0.0.10*8 dnsbl.sorbs.net=127.0.0.5*6 dnsbl.sorbs.net=127.0.0.7*3 dnsbl.sorbs.net=127.0.0.8*2 dnsbl.sorbs.net=127.0.0.6*2 dnsbl.sorbs.net=127.0.0.9*2 zen.spamhaus.org=127.0.0.[10;11]*8 zen.spamhaus.org=127.0.0.[4..7]*6 zen.spamhaus.org=127.0.0.3*4 zen.spamhaus.org=127.0.0.2*3 hostkarma.junkemailfilter.com=127.0.0.2*3 hostkarma.junkemailfilter.com=127.0.0.4*1 hostkarma.junkemailfilter.com=127.0.1.2*1 wl.mailspike.net=127.0.0.[18;19;20]*-2 hostkarma.junkemailfilter.com=127.0.0.1*-2
-postscreen_dnsbl_threshold = 8
+postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
+  hostkarma.junkemailfilter.com=127.0.0.1*-2
+  list.dnswl.org=127.0.[0..255].0*-2
+  list.dnswl.org=127.0.[0..255].1*-4
+  list.dnswl.org=127.0.[0..255].2*-6
+  list.dnswl.org=127.0.[0..255].3*-8
+  ix.dnsbl.manitu.net*2
+  bl.spamcop.net*2
+  hostkarma.junkemailfilter.com=127.0.0.2*4
+  hostkarma.junkemailfilter.com=127.0.0.3*2
+  hostkarma.junkemailfilter.com=127.0.0.4*3
+  hostkarma.junkemailfilter.com=127.0.1.2*1
+  backscatter.spameatingmonkey.net*2
+  bl.ipv6.spameatingmonkey.net*2
+  bl.spameatingmonkey.net*2
+  b.barracudacentral.org=127.0.0.2*7
+  bl.mailspike.net=127.0.0.2*5
+  bl.mailspike.net=127.0.0.[10;11;12]*4
+  dnsbl.sorbs.net=127.0.0.10*8
+  dnsbl.sorbs.net=127.0.0.5*6
+  dnsbl.sorbs.net=127.0.0.7*3
+  dnsbl.sorbs.net=127.0.0.8*2
+  dnsbl.sorbs.net=127.0.0.6*2
+  dnsbl.sorbs.net=127.0.0.9*2
+  zen.spamhaus.org=127.0.0.[10;11]*8
+  zen.spamhaus.org=127.0.0.[4..7]*6
+  zen.spamhaus.org=127.0.0.3*4
+  zen.spamhaus.org=127.0.0.2*3
+  hostkarma.junkemailfilter.com=127.0.0.2*3
+  hostkarma.junkemailfilter.com=127.0.0.4*2
+  hostkarma.junkemailfilter.com=127.0.1.2*1
+postscreen_dnsbl_threshold = 5
 postscreen_dnsbl_ttl = 5m
 postscreen_dnsbl_ttl = 5m
 postscreen_greet_action = enforce
 postscreen_greet_action = enforce
 postscreen_greet_banner = $smtpd_banner
 postscreen_greet_banner = $smtpd_banner
@@ -40,16 +79,10 @@ postscreen_greet_ttl = 2d
 postscreen_greet_wait = 3s
 postscreen_greet_wait = 3s
 postscreen_non_smtp_command_enable = no
 postscreen_non_smtp_command_enable = no
 postscreen_pipelining_enable = no
 postscreen_pipelining_enable = no
-proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf,
+proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf,
+  $sender_dependent_default_transport_maps,
+  $smtp_tls_policy_maps,
   $local_recipient_maps,
   $local_recipient_maps,
   $mydestination,
   $mydestination,
   $virtual_alias_maps,
   $virtual_alias_maps,
@@ -60,11 +93,14 @@ proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf,
   $relay_domains,
   $relay_domains,
   $canonical_maps,
   $canonical_maps,
   $sender_canonical_maps,
   $sender_canonical_maps,
+  $sender_bcc_maps,
+  $recipient_bcc_maps,
   $recipient_canonical_maps,
   $recipient_canonical_maps,
   $relocated_maps,
   $relocated_maps,
   $transport_maps,
   $transport_maps,
   $mynetworks,
   $mynetworks,
-  $smtpd_sender_login_maps
+  $smtpd_sender_login_maps,
+  $smtp_sasl_password_maps
 queue_run_delay = 300s
 queue_run_delay = 300s
 relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
 relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
 relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
 relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
@@ -81,33 +117,47 @@ smtpd_error_sleep_time = 10s
 smtpd_hard_error_limit = ${stress?1}${stress:5}
 smtpd_hard_error_limit = ${stress?1}${stress:5}
 smtpd_helo_required = yes
 smtpd_helo_required = yes
 smtpd_proxy_timeout = 600s
 smtpd_proxy_timeout = 600s
-smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, reject_invalid_helo_hostname, reject_unknown_reverse_client_hostname, reject_unauth_destination
+smtpd_recipient_restrictions = permit_sasl_authenticated,
+  permit_mynetworks,
+  check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
+  reject_invalid_helo_hostname,
+  reject_unknown_reverse_client_hostname,
+  reject_unauth_destination
 smtpd_sasl_auth_enable = yes
 smtpd_sasl_auth_enable = yes
 smtpd_sasl_authenticated_header = yes
 smtpd_sasl_authenticated_header = yes
 smtpd_sasl_path = inet:dovecot:10001
 smtpd_sasl_path = inet:dovecot:10001
 smtpd_sasl_type = dovecot
 smtpd_sasl_type = dovecot
 smtpd_sender_login_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
 smtpd_sender_login_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
-smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, permit_mynetworks, permit_sasl_authenticated, reject_unlisted_sender, reject_unknown_sender_domain
+smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
+  permit_mynetworks,
+  permit_sasl_authenticated,
+  reject_unlisted_sender,
+  reject_unknown_sender_domain
 smtpd_soft_error_limit = 3
 smtpd_soft_error_limit = 3
 smtpd_tls_auth_only = yes
 smtpd_tls_auth_only = yes
 smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
 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
+
+# Mandatory protocols and ciphers are used when a connections is enforced to use TLS
+# Does _not_ apply to enforced incoming TLS settings per mailbox
+smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
+smtpd_tls_mandatory_ciphers = high
+
 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_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
-smtpd_tls_mandatory_ciphers = high
 virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf,
 virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf,
-  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
 virtual_gid_maps = static:5000
 virtual_gid_maps = static:5000
 virtual_mailbox_base = /var/vmail/
 virtual_mailbox_base = /var/vmail/
 virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
 virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
@@ -123,7 +173,6 @@ smtpd_milters = inet:rspamd:9900
 non_smtpd_milters = inet:rspamd:9900
 non_smtpd_milters = inet:rspamd:9900
 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
 mydestination = localhost.localdomain, localhost
 mydestination = localhost.localdomain, localhost
-#content_filter=zeyple
 # Prefere IPv4, useful for v4-only envs
 # Prefere IPv4, useful for v4-only envs
 smtp_address_preference = ipv4
 smtp_address_preference = ipv4
 smtp_sender_dependent_authentication = yes
 smtp_sender_dependent_authentication = yes
@@ -134,5 +183,14 @@ 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
+# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
+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
+postscreen_discard_ehlo_keywords = silent-discard, dsn
+compatibility_level = 2
+smtputf8_enable = no
+
+# DO NOT EDIT ANYTHING BELOW #
+# User overrides #
+

+ 19 - 14
data/conf/postfix/master.cf

@@ -1,29 +1,47 @@
+# inter-mx with postscreen on 25/tcp
 smtp       inet  n       -       n       -       1       postscreen
 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
+
+# smtpd tls-wrapped (smtps) on 465/tcp
 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
+
+# smtpd with starttls on 587/tcp
 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
+
+# used by SOGo
+# smtpd_sender_restrictions should match main.cf, but with check_sasl_access prepended for login-as-mailbox-user function
 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
   -o smtpd_tls_auth_only=no
   -o smtpd_tls_auth_only=no
   -o smtpd_sender_restrictions=check_sasl_access,regexp:/opt/postfix/conf/allow_mailcow_local.regexp,reject_authenticated_sender_login_mismatch,permit_mynetworks,permit_sasl_authenticated,reject_unlisted_sender,reject_unknown_sender_domain
   -o smtpd_sender_restrictions=check_sasl_access,regexp:/opt/postfix/conf/allow_mailcow_local.regexp,reject_authenticated_sender_login_mismatch,permit_mynetworks,permit_sasl_authenticated,reject_unlisted_sender,reject_unknown_sender_domain
+
+# used to reinject quarantine mails
 590 inet n      -       n       -       -       smtpd
 590 inet n      -       n       -       -       smtpd
   -o smtpd_client_restrictions=permit_mynetworks,reject
   -o smtpd_client_restrictions=permit_mynetworks,reject
   -o smtpd_tls_auth_only=no
   -o smtpd_tls_auth_only=no
   -o smtpd_milters=
   -o smtpd_milters=
   -o non_smtpd_milters=
   -o non_smtpd_milters=
+
+# enforced smtp connector
 smtp_enforced_tls      unix  -       -       n       -       -       smtp
 smtp_enforced_tls      unix  -       -       n       -       -       smtp
   -o smtp_tls_security_level=encrypt
   -o smtp_tls_security_level=encrypt
   -o syslog_name=enforced-tls-smtp
   -o syslog_name=enforced-tls-smtp
   -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
   -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
+
+# smtp connector used, when a transport map matched
+# this helps to have different sasl maps than we have with sender dependent transport maps
 smtp_via_transport_maps      unix  -       -       n       -       -       smtp
 smtp_via_transport_maps      unix  -       -       n       -       -       smtp
   -o smtp_sasl_password_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
   -o smtp_sasl_password_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
 
 
@@ -55,25 +73,12 @@ scache     unix  -       -       n       -       1       scache
 maildrop   unix  -       n       n       -       -       pipe flags=DRhu
 maildrop   unix  -       n       n       -       -       pipe flags=DRhu
     user=vmail argv=/usr/bin/maildrop -d ${recipient}
     user=vmail argv=/usr/bin/maildrop -d ${recipient}
 
 
-# start zeyple
-zeyple    unix  -       n       n       -       -       pipe
-  user=zeyple argv=/usr/local/bin/zeyple.py ${recipient}
-127.0.0.1:10026 inet  n       -       n       -       10      smtpd
-  -o content_filter=
-  -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
-  -o smtpd_helo_restrictions=
-  -o smtpd_client_restrictions=
-  -o smtpd_sender_restrictions=
-  -o smtpd_recipient_restrictions=permit_mynetworks,reject
-  -o mynetworks=127.0.0.0/8
-  -o smtpd_authorized_xforward_hosts=127.0.0.0/8
-# end zeyple
-
 # start whitelist_fwd
 # start whitelist_fwd
 127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh
 127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh
 # end whitelist_fwd
 # end whitelist_fwd
 
 
 # start watchdog-specific
 # start watchdog-specific
+# logs to local7 (hidden)
 589 inet n      -       n       -       -       smtpd
 589 inet n      -       n       -       -       smtpd
   -o smtpd_client_restrictions=permit_mynetworks,reject
   -o smtpd_client_restrictions=permit_mynetworks,reject
   -o syslog_name=watchdog
   -o syslog_name=watchdog

+ 44 - 0
data/conf/rspamd/custom/bad_words.map

@@ -0,0 +1,44 @@
+/\ssex\s/i
+/\svagina\s/i
+/\serotic\s/i
+/\serection\s/i
+/\ssexy\s/i
+/\spenis\s/i
+/\sass\s/i
+/\sviagra\s/i
+/\stits\s/i
+/\stitty\s/i
+/\stitties\s/i
+/\scum\s/i
+/\ssperm\s/i
+/\sslut\s/i
+/\sporn\s/i
+/\scock\s/i
+/\spharma\s/i
+/\spharmacy\s/i
+/\sseo\s/i
+/\smarketing\s/i
+/\sjackpot\s/i
+/\slotto\s/i
+/\slottery\s/i
+/pillenversand/i
+/\skredithilfe\s/i
+/\skapital\s/i
+/\skrankenversicherung\s/i
+/bitcoin/i
+/pädophil/i
+/paedophil/i
+/freiberufler/i
+/unternehmer/i
+/masturbieren/i
+/trojaner/i
+/malware/i
+/\sscooter\s/i
+/\sescooter\s/i
+/\se-scooter\s/i
+/testost/i
+/\spotenz\s/i
+/potenzmittel/i
+/rezeptfrei/i
+/apotheke/i
+/web\sdevelopment/i

+ 66 - 0
data/conf/rspamd/custom/fishy_tlds.map

@@ -0,0 +1,66 @@
+/.+\.accountant$/i
+/.+\.art$/i
+/.+\.asia$/i
+/.+\.bid$/i
+/.+\.biz$/i
+/.+\.care$/i
+/.+\.cf$/i
+/.+\.cl$/i
+/.+\.click$/i
+/.+\.cloud$/i
+/.+\.co$/i
+/.+\.construction$/i
+/.+\.country$/i
+/.+\.cricket$/i
+/.+\.date$/i
+/.+\.desi$/i
+/.+\.download$/i
+/.+\.estate$/i
+/.+\.faith$/i
+/.+\.fit$/i
+/.+\.flights$/i
+/.+\.ga$/i
+/.+\.gdn$/i
+/.+\.gq$/i
+/.+\.guru$/i
+/.+\.icu$/i
+/.+\.id$/i
+/.+\.info$/i
+/.+\.in.net$/i
+/.+\.ir$/i
+/.+\.jetzt$/i
+/.+\.kim$/i
+/.+\.life$/i
+/.+\.link$/i
+/.+\.loan$/i
+/.+\.mk$/i
+/.+\.ml$/i
+/.+\.ninja$/i
+/.+\.online$/i
+/.+\.ooo$/i
+/.+\.party$/i
+/.+\.pro$/i
+/.+\.ps$/i
+/.+\.pw$/i
+/.+\.racing$/i
+/.+\.review$/i
+/.+\.rocks$/i
+/.+\.ryukyu$/i
+/.+\.science$/i
+/.+\.site$/i
+/.+\.space$/i
+/.+\.stream$/i
+/.+\.sucks$/i
+/.+\.tk$/i
+/.+\.top$/i
+/.+\.topica\.com$/i
+/.+\.town$/i
+/.+\.trade$/i
+/.+\.uno$/i
+/.+\.vip$/i
+/.+\.webcam$/i
+/.+\.website$/i
+/.+\.win$/i
+/.+\.work$/i
+/.+\.world$/i
+/.+\.xyz$/i

+ 4 - 0
data/conf/rspamd/custom/ip_wl.map

@@ -0,0 +1,4 @@
+# IP whitelist
+# 127.0.0.1
+# 1.2.3.4
+# ...

+ 35 - 35
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, '@');
@@ -43,7 +62,9 @@ function wl_by_sogo() {
       if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) {
       if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) {
         continue;
         continue;
       }
       }
-      $rcpt[$row['user']][] = '/^' . str_replace('/', '\/', $contact) . '$/i';
+      // Explicit from, no mime_from, no regex - envelope must match
+      // mailcow white and blacklists also cover mime_from
+      $rcpt[$row['user']][] = str_replace('/', '\/', $contact);
     }
     }
   }
   }
   return $rcpt;
   return $rcpt;
@@ -67,7 +88,7 @@ function ucl_rcpts($object, $type) {
       if (!empty($local) && !empty($domain)) {
       if (!empty($local) && !empty($domain)) {
         $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
         $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
       }
       }
-      $rcpt[] = '/^' . str_replace('/', '\/', $row['address']) . '$/i';
+      $rcpt[] = str_replace('/', '\/', $row['address']);
     }
     }
     // Aliases by alias domains
     // Aliases by alias domains
     $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` 
     $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` 
@@ -85,7 +106,7 @@ function ucl_rcpts($object, $type) {
         if (!empty($local) && !empty($domain)) {
         if (!empty($local) && !empty($domain)) {
           $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
           $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
         }
         }
-      $rcpt[] = '/^' . str_replace('/', '\/', $row['alias']) . '$/i';
+        $rcpt[] = str_replace('/', '\/', $row['alias']);
       }
       }
     }
     }
   }
   }
@@ -107,8 +128,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 +220,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 +259,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 +308,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 +349,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;

+ 2 - 0
data/conf/rspamd/local.d/arc.conf

@@ -28,3 +28,5 @@ use_redis = true;
 key_prefix = "DKIM_PRIV_KEYS";
 key_prefix = "DKIM_PRIV_KEYS";
 # Selector map
 # Selector map
 selector_prefix = "DKIM_SELECTORS";
 selector_prefix = "DKIM_SELECTORS";
+sign_inbound = true;
+use_domain_sign_inbound = "recipient";

+ 14 - 0
data/conf/rspamd/local.d/composites.conf

@@ -16,3 +16,17 @@ SOGO_CONTACT_EXCLUDE_FWD_HOST {
 SOGO_CONTACT_SPOOFED {
 SOGO_CONTACT_SPOOFED {
   expression = "(R_SPF_PERMFAIL | R_SPF_SOFTFAIL | R_SPF_FAIL) & ~SOGO_CONTACT";
   expression = "(R_SPF_PERMFAIL | R_SPF_SOFTFAIL | R_SPF_FAIL) & ~SOGO_CONTACT";
 }
 }
+SPOOFED_UNAUTH {
+  expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & !R_SPF_ALLOW & !DMARC_POLICY_ALLOW & !ARC_ALLOW & !SIEVE_HOST & MAILCOW_DOMAIN_HEADER_FROM";
+  score = 5.0;
+}
+# Only apply to inbound unauthed and not whitelisted
+OLEFY_MACRO {
+  expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & OLETOOLS";
+  score = 20.0;
+  policy = "remove_weight";
+}
+BAD_WORD_BAD_TLD {
+  expression = "FISHY_TLD & BAD_WORDS"
+  score = 10.0;
+}

+ 7 - 0
data/conf/rspamd/local.d/external_services.conf

@@ -0,0 +1,7 @@
+oletools {
+  # default olefy settings
+  servers = "olefy:10055";
+  # needs to be set explicitly for Rspamd < 1.9.5
+  scan_mime_parts = true;
+  # mime-part regex matching in content-type or filename
+}

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

@@ -20,7 +20,7 @@ return function(task)
   if ratelimited then
   if ratelimited then
     return true
     return true
   end
   end
-  return
+  return false
 end
 end
 EOD;
 EOD;
 }
 }

+ 1 - 0
data/conf/rspamd/local.d/milter_headers.conf

@@ -13,6 +13,7 @@ routines {
   authentication-results {
   authentication-results {
     header = "Authentication-Results";
     header = "Authentication-Results";
     remove = 1;
     remove = 1;
+    add_smtp_user = false;
     spf_symbols {
     spf_symbols {
       pass = "R_SPF_ALLOW";
       pass = "R_SPF_ALLOW";
       fail = "R_SPF_FAIL";
       fail = "R_SPF_FAIL";

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

@@ -83,3 +83,39 @@ GLOBAL_RCPT_BL {
   prefilter = true;
   prefilter = true;
   action = "reject";
   action = "reject";
 }
 }
+
+SIEVE_HOST {
+  type = "ip";
+  map = "$LOCAL_CONFDIR/custom/dovecot_trusted.map";
+  symbols_set = ["SIEVE_HOST"];
+}
+
+MAILCOW_DOMAIN_HEADER_FROM { 
+  type = "header";  
+  header = "from";  
+  filter = "email:domain";  
+  map = "redis://DOMAIN_MAP"; 
+}
+
+IP_WHITELIST {
+  type = "ip";
+  map = "$LOCAL_CONFDIR/custom/ip_wl.map";
+  prefilter = "true";
+  action = "accept";
+}
+
+FISHY_TLD {
+  type = "from";
+  filter = "email:domain";
+  map = "${LOCAL_CONFDIR}/custom/fishy_tlds.map";
+  regexp = true;
+  score = 0.1;
+}
+
+BAD_WORDS {
+  type = "content";
+  filter = "text";
+  map = "${LOCAL_CONFDIR}/custom/bad_words.map";
+  regexp = true;
+  score = 0.1;
+}

+ 8 - 2
data/conf/rspamd/local.d/policies_group.conf

@@ -11,7 +11,13 @@ symbols = {
     "R_DKIM_REJECT" {
     "R_DKIM_REJECT" {
         score = 10.0;
         score = 10.0;
     }
     }
-    "R_DKIM_PERMFAIL" {
-        score = 10.0;
+    "DMARC_POLICY_REJECT" {
+        weight = 20.0;
+    }
+    "DMARC_POLICY_QUARANTINE" {
+        weight = 10.0;
+    }
+    "DMARC_POLICY_SOFTFAIL" {
+        weight = 2.0;
     }
     }
 }
 }

+ 10 - 0
data/conf/rspamd/local.d/rbl.conf

@@ -0,0 +1,10 @@
+rbls {
+  uceprotect1 {
+    symbol = "RBL_UCEPROTECT_LEVEL1";
+    rbl = "dnsbl-1.uceprotect.net";
+  }
+  uceprotect2 {
+    symbol = "RBL_UCEPROTECT_LEVEL2";
+    rbl = "dnsbl-2.uceprotect.net";
+  }
+}

+ 8 - 0
data/conf/rspamd/local.d/rbl_group.conf

@@ -0,0 +1,8 @@
+symbols = {
+  "RBL_UCEPROTECT_LEVEL1" {
+    score = 3.5;
+  }
+  "RBL_UCEPROTECT_LEVEL2" {
+    score = 1.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;
-}
-

+ 1 - 0
data/conf/rspamd/local.d/spamassassin.conf

@@ -0,0 +1 @@
+ruleset = "/etc/rspamd/custom/sa-rules";

+ 2 - 2
data/conf/rspamd/local.d/statistics_group.conf

@@ -1,10 +1,10 @@
 symbols = {
 symbols = {
     "BAYES_SPAM" {
     "BAYES_SPAM" {
-        weight = 8.5;
+        weight = 2.5;
         description = "Message probably spam, probability: ";
         description = "Message probably spam, probability: ";
     }
     }
     "BAYES_HAM" {
     "BAYES_HAM" {
-        weight = -12.5;
+        weight = -10.5;
         description = "Message probably ham, probability: ";
         description = "Message probably ham, probability: ";
     }
     }
 }
 }

+ 23 - 2
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);
   
   
@@ -128,6 +131,14 @@ foreach (json_decode($rcpts, true) as $rcpt) {
       ));
       ));
       $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
       $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
     }
     }
+    if (empty($gotos)) {
+      $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
+      $stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
+      $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+      if ($goto_branch) {
+        $gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
+      }
+    }
     $gotos_array = explode(',', $gotos);
     $gotos_array = explode(',', $gotos);
 
 
     $loop_c = 0;
     $loop_c = 0;
@@ -156,8 +167,18 @@ foreach (json_decode($rcpts, true) as $rcpt) {
             $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
             $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
             $stmt->execute(array(':goto' => $goto));
             $stmt->execute(array(':goto' => $goto));
             $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
             $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
-            error_log("QUARANTINE: quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch);
-            $goto_branch_array = explode(',', $goto_branch);
+            if ($goto_branch) {
+              error_log("QUARANTINE: quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch);
+              $goto_branch_array = explode(',', $goto_branch);
+            } else {
+              $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
+              $stmt->execute(array(':domain' => $parsed_goto['domain']));
+              $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
+              if ($goto_branch) {
+                error_log("QUARANTINE: quarantine pipe: goto domain " . $parsed_gto['domain'] . " is a domain alias branch for " . $goto_branch);
+                $goto_branch_array = array($parsed_gto['local'] . '@' . $goto_branch);
+              }
+            }
           }
           }
         }
         }
         // goto item was processed, unset
         // goto item was processed, unset

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

@@ -1,8 +1,8 @@
 rates {
 rates {
     # Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default
     # Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default
-    to = "45 / 1m";
-    to_ip = "360 / 1m";
-    to_ip_from = "180 / 1m";
+    to = "100 / 1s";
+    to_ip = "100 / 1s";
+    to_ip_from = "100 / 1s";
     bounce_to = "100 / 1s";
     bounce_to = "100 / 1s";
     bounce_to_ip = "100 / 1s";
     bounce_to_ip = "100 / 1s";
 }
 }

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

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

+ 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/conf/rspamd/plugins.d/README.md

@@ -0,0 +1 @@
+This is where you should copy any rspamd custom module

+ 1 - 0
data/conf/rspamd/rspamd.conf.local

@@ -0,0 +1 @@
+# rspamd.conf.local

+ 2 - 0
data/conf/rspamd/rspamd.conf.override

@@ -0,0 +1,2 @@
+# rspamd.conf.override
+

+ 0 - 1
data/conf/sogo/sogo.conf

@@ -26,7 +26,6 @@
     //  (domain3.tld, domain2.tld)
     //  (domain3.tld, domain2.tld)
     // );
     // );
 
 
-    SOGoIMAPServer = "imap://dovecot:143/?tls=YES";
     SOGoSieveServer = "sieve://dovecot:4190/?tls=YES";
     SOGoSieveServer = "sieve://dovecot:4190/?tls=YES";
     SOGoSMTPServer = "postfix:588";
     SOGoSMTPServer = "postfix:588";
     WOPort = "0.0.0.0:20000";
     WOPort = "0.0.0.0:20000";

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

@@ -32,6 +32,7 @@ server:
   hide-version: yes
   hide-version: yes
   max-udp-size: 4096
   max-udp-size: 4096
   msg-buffer-size: 65552
   msg-buffer-size: 65552
+  unwanted-reply-threshold: 10000
 
 
 remote-control:
 remote-control:
     control-enable: yes
     control-enable: yes

+ 71 - 16
data/web/admin.php

@@ -5,6 +5,9 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 $tfa_data = get_tfa();
 $tfa_data = get_tfa();
+if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
+  $_SESSION['gal'] = json_decode($license_cache, true);
+}
 ?>
 ?>
 <div class="container">
 <div class="container">
 
 
@@ -76,8 +79,40 @@ $tfa_data = get_tfa();
             </select>
             </select>
           </div>
           </div>
         </div>
         </div>
-        <legend data-target="#api" style="margin-top:40px;cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse">
-          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> API (experimental, work in progress)
+
+        <legend data-target="#license" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['guid_and_license'];?>
+        </legend>
+        <div id="license" class="collapse in">
+        <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="guid"><?=$lang['admin']['guid'];?>:</label>
+            <div class="col-sm-9">
+              <div class="input-group">
+                <span class="input-group-addon">
+                  <span class="glyphicon <?=(isset($_SESSION['gal']['valid']) && $_SESSION['gal']['valid'] === "true") ? 'glyphicon-heart text-danger' : 'glyphicon-remove';?>" aria-hidden="true"></span>
+                </span>
+                <input type="text" id="guid" class="form-control" value="<?=license('guid');?>" readonly>
+              </div>
+              <p class="help-block">
+                <?=$lang['admin']['customer_id'];?>: <?=(isset($_SESSION['gal']['c'])) ? $_SESSION['gal']['c'] : '?';?> -
+                <?=$lang['admin']['service_id'];?>: <?=(isset($_SESSION['gal']['s'])) ? $_SESSION['gal']['s'] : '?';?>
+              </p>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <p class="help-block"><?=$lang['admin']['license_info'];?></p>
+              <div class="btn-group">
+                <button class="btn btn-sm btn-success" name="license_validate_now" type="submit" href="#"><?=$lang['admin']['validate_license_now'];?></button>
+              </div>
+            </div>
+          </div>
+        </form>
+        </div>
+
+        <legend data-target="#api" class="arrow-toggle" unselectable="on" data-toggle="collapse">
+          <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> API
         </legend>
         </legend>
         <?php
         <?php
         $api = admin_api('get');
         $api = admin_api('get');
@@ -105,6 +140,7 @@ $tfa_data = get_tfa();
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
             <div class="col-sm-offset-3 col-sm-9">
             <div class="col-sm-offset-3 col-sm-9">
+              <p class="help-block"><?=$lang['admin']['api_info'];?></p>
               <div class="btn-group">
               <div class="btn-group">
                 <button class="btn btn-default" name="admin_api" type="submit" href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
                 <button class="btn btn-default" name="admin_api" type="submit" href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
                 <button class="btn btn-info" name="admin_api_regen_key" type="submit" href="#"><?=$lang['admin']['regen_api_key'];?></button>
                 <button class="btn btn-info" name="admin_api_regen_key" type="submit" href="#"><?=$lang['admin']['regen_api_key'];?></button>
@@ -113,6 +149,7 @@ $tfa_data = get_tfa();
           </div>
           </div>
         </form>
         </form>
         </div>
         </div>
+
       </div>
       </div>
     </div>
     </div>
 
 
@@ -252,7 +289,7 @@ $tfa_data = get_tfa();
             <form class="form" data-id="transport" role="form" method="post">
             <form class="form" data-id="transport" role="form" method="post">
               <div class="form-group">
               <div class="form-group">
                 <label for="destination"><?=$lang['admin']['destination'];?></label>
                 <label for="destination"><?=$lang['admin']['destination'];?></label>
-                <input class="form-control input-sm" name="destination" placeholder='example.org, .example.org, *, box@example.org' required>
+                <input class="form-control input-sm" name="destination" placeholder='<?=$lang['admin']['transport_dest_format'];?>' required>
               </div>
               </div>
               <div class="form-group">
               <div class="form-group">
                 <label for="nexthop"><?=$lang['admin']['nexthop'];?></label>
                 <label for="nexthop"><?=$lang['admin']['nexthop'];?></label>
@@ -266,6 +303,16 @@ $tfa_data = get_tfa();
                 <label for="password"><?=$lang['admin']['password'];?></label>
                 <label for="password"><?=$lang['admin']['password'];?></label>
                 <input class="form-control" name="password">
                 <input class="form-control" name="password">
               </div>
               </div>
+              <!-- <div class="form-group">
+                <label>
+                  <input type="checkbox" name="lookup_mx" value="1"> <?=$lang['admin']['lookup_mx'];?>
+                </label>
+              </div> -->
+              <div class="form-group">
+                <label>
+                  <input type="checkbox" name="active" value="1"> <?=$lang['admin']['active'];?>
+                </label>
+              </div>
               <p class="help-block"><?=$lang['admin']['credentials_transport_warning'];?></p>
               <p class="help-block"><?=$lang['admin']['credentials_transport_warning'];?></p>
               <button class="btn btn-default" data-action="add_item" data-id="transport" data-api-url='add/transport' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
               <button class="btn btn-default" data-action="add_item" data-id="transport" data-api-url='add/transport' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
             </form>
             </form>
@@ -326,7 +373,7 @@ $tfa_data = get_tfa();
           else {
           else {
           ?>
           ?>
           <div class="row">
           <div class="row">
-              <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
+            <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
             <div class="col-md-3">
             <div class="col-md-3">
               <p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
               <p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
             </div>
             </div>
@@ -600,13 +647,14 @@ $tfa_data = get_tfa();
           </span></p>
           </span></p>
           <?php
           <?php
           endforeach;
           endforeach;
+          ?>
+          <hr>
+          <?php
         endif;
         endif;
         if (!empty($f2b_data['perm_bans'])):
         if (!empty($f2b_data['perm_bans'])):
           foreach ($f2b_data['perm_bans'] as $perm_bans):
           foreach ($f2b_data['perm_bans'] as $perm_bans):
           ?>
           ?>
-          <p>
-          <span class="label label-danger" style="padding:4px;font-size:85%;"><span class="glyphicon glyphicon-filter"></span> <?=$perm_bans?></span>
-          </p>
+          <span class="label label-danger" style="padding: 0.1em 0.4em 0.1em;"><span class="glyphicon glyphicon-filter"></span> <?=$perm_bans?></span>
           <?php
           <?php
           endforeach;
           endforeach;
         endif;
         endif;
@@ -621,30 +669,36 @@ $tfa_data = get_tfa();
        <?php $q_data = quarantine('settings');?>
        <?php $q_data = quarantine('settings');?>
         <form class="form" data-id="quarantine" role="form" method="post">
         <form class="form" data-id="quarantine" role="form" method="post">
           <div class="row">
           <div class="row">
-            <div class="col-sm-6">
+            <div class="col-sm-4">
               <div class="form-group">
               <div class="form-group">
                 <label for="retention_size"><?=$lang['admin']['quarantine_retention_size'];?></label>
                 <label for="retention_size"><?=$lang['admin']['quarantine_retention_size'];?></label>
                 <input type="number" class="form-control" name="retention_size" value="<?=$q_data['retention_size'];?>" placeholder="0" required>
                 <input type="number" class="form-control" name="retention_size" value="<?=$q_data['retention_size'];?>" placeholder="0" required>
               </div>
               </div>
             </div>
             </div>
-            <div class="col-sm-6">
+            <div class="col-sm-4">
               <div class="form-group">
               <div class="form-group">
                 <label for="max_size"><?=$lang['admin']['quarantine_max_size'];?></label>
                 <label for="max_size"><?=$lang['admin']['quarantine_max_size'];?></label>
                 <input type="number" class="form-control" name="max_size" value="<?=$q_data['max_size'];?>" placeholder="0" required>
                 <input type="number" class="form-control" name="max_size" value="<?=$q_data['max_size'];?>" placeholder="0" required>
               </div>
               </div>
             </div>
             </div>
+            <div class="col-sm-4">
+              <div class="form-group">
+                <label for="max_age"><?=$lang['admin']['quarantine_max_age'];?></label>
+                <input type="number" class="form-control" name="max_age" value="<?=$q_data['max_age'];?>" min="1" required>
+              </div>
+            </div>
           </div>
           </div>
           <div class="row">
           <div class="row">
             <div class="col-sm-6">
             <div class="col-sm-6">
               <div class="form-group">
               <div class="form-group">
                 <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label>
                 <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label>
-                <input type="text" class="form-control" name="sender" value="<?=$q_data['sender'];?>" placeholder="quarantine@localhost">
+                <input type="text" class="form-control" name="sender" value="<?=htmlspecialchars($q_data['sender']);?>" placeholder="quarantine@localhost">
               </div>
               </div>
             </div>
             </div>
             <div class="col-sm-6">
             <div class="col-sm-6">
               <div class="form-group">
               <div class="form-group">
                 <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label>
                 <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label>
-                <input type="text" class="form-control" name="subject" value="<?=$q_data['subject'];?>" placeholder="Spam Quarantine Notification">
+                <input type="text" class="form-control" name="subject" value="<?=htmlspecialchars($q_data['subject']);?>" placeholder="Spam Quarantine Notification">
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
@@ -699,13 +753,13 @@ $tfa_data = get_tfa();
           <div class="col-sm-6">
           <div class="col-sm-6">
             <div class="form-group">
             <div class="form-group">
               <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label>
               <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label>
-              <input type="text" class="form-control" name="sender" value="<?=$qw_data['sender'];?>" placeholder="quota-warning@localhost">
+              <input type="text" class="form-control" name="sender" value="<?=htmlspecialchars($qw_data['sender']);?>" placeholder="quota-warning@localhost">
             </div>
             </div>
           </div>
           </div>
           <div class="col-sm-6">
           <div class="col-sm-6">
             <div class="form-group">
             <div class="form-group">
               <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label>
               <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label>
-              <input type="text" class="form-control" name="subject" value="<?=$qw_data['subject'];?>" placeholder="Quota warning">
+              <input type="text" class="form-control" name="subject" value="<?=htmlspecialchars($qw_data['subject']);?>" placeholder="Quota warning">
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -746,6 +800,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">
@@ -796,11 +851,11 @@ $tfa_data = get_tfa();
                     <input type="hidden" name="active" value="0">
                     <input type="hidden" name="active" value="0">
                     <div class="form-group">
                     <div class="form-group">
                       <label for="desc"><?=$lang['admin']['rsetting_desc'];?>:</label>
                       <label for="desc"><?=$lang['admin']['rsetting_desc'];?>:</label>
-                      <input type="text" class="form-control" name="desc" value="<?=$rsetting_details['desc'];?>">
+                      <input type="text" class="form-control" name="desc" value="<?=htmlspecialchars($rsetting_details['desc']);?>">
                     </div>
                     </div>
                     <div class="form-group">
                     <div class="form-group">
                       <label for="content"><?=$lang['admin']['rsetting_content'];?>:</label>
                       <label for="content"><?=$lang['admin']['rsetting_content'];?>:</label>
-                      <textarea class="form-control" name="content" rows="10"><?=$rsetting_details['content'];?></textarea>
+                      <textarea class="form-control" name="content" rows="10"><?=htmlspecialchars($rsetting_details['content']);?></textarea>
                     </div>
                     </div>
                     <div class="form-group">
                     <div class="form-group">
                       <label>
                       <label>
@@ -876,7 +931,7 @@ $tfa_data = get_tfa();
               <td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required value="<?=$val;?>"></td>
               <td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required value="<?=$val;?>"></td>
               <td><a href="#" role="button" class="btn btn-xs btn-default" type="button"><?=$lang['admin']['remove_row'];?></a></td>
               <td><a href="#" role="button" class="btn btn-xs btn-default" type="button"><?=$lang['admin']['remove_row'];?></a></td>
             </tr>
             </tr>
-            <?php 
+            <?php
               endforeach;
               endforeach;
             }
             }
             foreach ($MAILCOW_APPS as $app):
             foreach ($MAILCOW_APPS as $app):

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


+ 1 - 1
data/web/css/build/004-bootstrap-slider.min.css

@@ -1,5 +1,5 @@
 /*! =======================================================
 /*! =======================================================
-                      VERSION  10.6.0              
+                      VERSION  10.6.1              
 ========================================================= */
 ========================================================= */
 /*! =========================================================
 /*! =========================================================
  * bootstrap-slider.js
  * bootstrap-slider.js

+ 3 - 0
data/web/css/build/008-mailcow.css

@@ -42,6 +42,9 @@
 .btn {
 .btn {
   text-transform: none;
   text-transform: none;
 }
 }
+.btn * {
+  pointer-events: none;
+}
 .textarea-code {
 .textarea-code {
   font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
   font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
   background:transparent !important;
   background:transparent !important;

+ 7 - 1
data/web/css/site/admin.css

@@ -70,4 +70,10 @@ body.modal-open {
 }
 }
 .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
 .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
   padding: 3px;
   padding: 3px;
-}
+}
+table tbody tr {
+  cursor: pointer;
+}
+table tbody tr td input[type="checkbox"] {
+  cursor: pointer;
+}

+ 5 - 0
data/web/css/site/index.css

@@ -0,0 +1,5 @@
+@media (max-width: 500px) {
+  #top {
+    padding-top: 15px !important;
+  }
+}

+ 7 - 1
data/web/css/site/mailbox.css

@@ -9,7 +9,7 @@ table.footable>tbody>tr.footable-empty>td {
   overflow: visible !important;
   overflow: visible !important;
 }
 }
 .table-responsive {
 .table-responsive {
-  overflow: auto !important;
+  overflow: inherit !important;
 }
 }
 @media screen and (max-width: 767px) {
 @media screen and (max-width: 767px) {
   .table-responsive {
   .table-responsive {
@@ -53,3 +53,9 @@ table.footable>tbody>tr.footable-empty>td {
   font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
   font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
   font-size:smaller;
   font-size:smaller;
 }
 }
+table tbody tr {
+  cursor: pointer;
+}
+table tbody tr td input[type="checkbox"] {
+  cursor: pointer;
+}

+ 17 - 1
data/web/css/site/quarantine.css

@@ -48,4 +48,20 @@ table.footable>tbody>tr.footable-empty>td {
   background-color: #d4d4d4;
   background-color: #d4d4d4;
   border-radius: 50%;
   border-radius: 50%;
   display: inline-block;
   display: inline-block;
-}
+}
+
+span.mail-address-item {
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  border: 1px solid #ccc;
+  padding: 2px 7px;
+  margin-right: 7px;
+}
+
+table tbody tr {
+  cursor: pointer;
+}
+
+table tbody tr td input[type="checkbox"] {
+  cursor: pointer;
+}

+ 8 - 0
data/web/css/site/user.css

@@ -39,4 +39,12 @@ table.footable>tbody>tr.footable-empty>td {
 }
 }
 body {
 body {
   overflow-y:scroll;
   overflow-y:scroll;
+}
+
+table tbody tr {
+  cursor: pointer;
+}
+
+table tbody tr td input[type="checkbox"] {
+  cursor: pointer;
 }
 }

+ 71 - 13
data/web/edit.php

@@ -273,6 +273,12 @@ if (isset($_SESSION['mailcow_cc_role'])) {
                 <input type="number" class="form-control" name="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
                 <input type="number" class="form-control" name="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
               </div>
               </div>
             </div>
             </div>
+            <div class="form-group">
+                <label class="control-label col-sm-2" for="defquota"><?=$lang['edit']['mailbox_quota_def'];?></label>
+                <div class="col-sm-10">
+                    <input type="number" class="form-control" name="defquota" value="<?=intval($result['def_quota_for_mbox'] / 1048576);?>">
+                </div>
+            </div>
             <div class="form-group">
             <div class="form-group">
               <label class="control-label col-sm-2" for="maxquota"><?=$lang['edit']['max_quota'];?></label>
               <label class="control-label col-sm-2" for="maxquota"><?=$lang['edit']['max_quota'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
@@ -379,7 +385,6 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
             <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
               <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
               <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
-              </ul>
             </div>
             </div>
           </div>
           </div>
           <form class="form-inline" data-id="add_wl_policy_domain">
           <form class="form-inline" data-id="add_wl_policy_domain">
@@ -401,7 +406,6 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
             <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>">
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
               <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
               <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
               <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li>
-              </ul>
             </div>
             </div>
           </div>
           </div>
           <form class="form-inline" data-id="add_bl_policy_domain">
           <form class="form-inline" data-id="add_bl_policy_domain">
@@ -502,6 +506,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
       $mailbox = html_entity_decode(rawurldecode($_GET["mailbox"]));
       $mailbox = html_entity_decode(rawurldecode($_GET["mailbox"]));
       $result = mailbox('get', 'mailbox_details', $mailbox);
       $result = mailbox('get', 'mailbox_details', $mailbox);
       $rl = ratelimit('get', 'mailbox', $mailbox);
       $rl = ratelimit('get', 'mailbox', $mailbox);
+      $quarantine_notification = mailbox('get', 'quarantine_notification', $mailbox);
       if (!empty($result)) {
       if (!empty($result)) {
         ?>
         ?>
         <h4><?=$lang['edit']['mailbox'];?></h4>
         <h4><?=$lang['edit']['mailbox'];?></h4>
@@ -511,21 +516,22 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <input type="hidden" value="0" name="force_pw_update">
           <input type="hidden" value="0" name="force_pw_update">
           <input type="hidden" value="0" name="sogo_access">
           <input type="hidden" value="0" name="sogo_access">
           <div class="form-group">
           <div class="form-group">
-            <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?>:</label>
+            <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?></label>
             <div class="col-sm-10">
             <div class="col-sm-10">
             <input type="text" class="form-control" name="name" value="<?=htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8');?>">
             <input type="text" class="form-control" name="name" value="<?=htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8');?>">
             </div>
             </div>
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
-            <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['quota_mb'];?>:
+            <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['quota_mb'];?>
               <br /><span id="quotaBadge" class="badge">max. <?=intval($result['max_new_quota'] / 1048576)?> MiB</span>
               <br /><span id="quotaBadge" class="badge">max. <?=intval($result['max_new_quota'] / 1048576)?> MiB</span>
             </label>
             </label>
             <div class="col-sm-10">
             <div class="col-sm-10">
-              <input type="number" name="quota" style="width:100%" min="1" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control">
+              <input type="number" name="quota" style="width:100%" min="0" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control">
+              <small class="help-block">0 = ∞</small>
             </div>
             </div>
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
-            <label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?>:</label>
+            <label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?></label>
             <div class="col-sm-10">
             <div class="col-sm-10">
               <select data-live-search="true" data-width="100%" style="width:100%" id="editSelectSenderACL" name="sender_acl" size="10" multiple>
               <select data-live-search="true" data-width="100%" style="width:100%" id="editSelectSenderACL" name="sender_acl" size="10" multiple>
               <?php
               <?php
@@ -537,7 +543,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
                 <?php
                 <?php
               endforeach;
               endforeach;
 
 
-              foreach ($sender_acl_handles['sender_acl_addresses']['ro'] as $domain):
+              foreach ($sender_acl_handles['sender_acl_addresses']['ro'] as $alias):
                 ?>
                 ?>
               <option data-subtext="Admin" disabled selected><?=htmlspecialchars($alias);?></option>
               <option data-subtext="Admin" disabled selected><?=htmlspecialchars($alias);?></option>
                 <?php
                 <?php
@@ -573,9 +579,51 @@ if (isset($_SESSION['mailcow_cc_role'])) {
                 <?php
                 <?php
               endforeach;
               endforeach;
 
 
+              // Generated here, but used in extended_sender_acl
+              if (!empty($sender_acl_handles['external_sender_aliases'])) {
+                $ext_sender_acl = implode(', ', $sender_acl_handles['external_sender_aliases']);
+              }
+              else {
+                $ext_sender_acl = '';
+              }
+
               ?>
               ?>
               </select>
               </select>
               <div style="display:none" id="sender_acl_disabled"><?=$lang['edit']['sender_acl_disabled'];?></div>
               <div style="display:none" id="sender_acl_disabled"><?=$lang['edit']['sender_acl_disabled'];?></div>
+              <small class="help-block"><?=$lang['edit']['sender_acl_info'];?></small>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="sender_acl"><?=$lang['user']['quarantine_notification'];?></label>
+            <div class="col-sm-10">
+            <div class="btn-group" data-acl="<?=$_SESSION['acl']['quarantine_notification'];?>">
+              <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "never") ? "active" : null;?>"
+                data-action="edit_selected"
+                data-item="<?= htmlentities($mailbox); ?>"
+                data-id="quarantine_notification"
+                data-api-url='edit/quarantine_notification'
+                data-api-attr='{"quarantine_notification":"never"}'><?=$lang['user']['never'];?></button>
+              <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "hourly") ? "active" : null;?>"
+                data-action="edit_selected"
+                data-item="<?= htmlentities($mailbox); ?>"
+                data-id="quarantine_notification"
+                data-api-url='edit/quarantine_notification'
+                data-api-attr='{"quarantine_notification":"hourly"}'><?=$lang['user']['hourly'];?></button>
+              <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "daily") ? "active" : null;?>"
+                data-action="edit_selected"
+                data-item="<?= htmlentities($mailbox); ?>"
+                data-id="quarantine_notification"
+                data-api-url='edit/quarantine_notification'
+                data-api-attr='{"quarantine_notification":"daily"}'><?=$lang['user']['daily'];?></button>
+              <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "weekly") ? "active" : null;?>"
+                data-action="edit_selected"
+                data-item="<?= htmlentities($mailbox); ?>"
+                data-id="quarantine_notification"
+                data-api-url='edit/quarantine_notification'
+                data-api-attr='{"quarantine_notification":"weekly"}'><?=$lang['user']['weekly'];?></button>
+            </div>
+            <div style="display:none" id="user_acl_q_notify_disabled"><?=$lang['edit']['user_acl_q_notify_disabled'];?></div>
+            <p class="help-block"><small><?=$lang['user']['quarantine_notification_info'];?></small></p>
             </div>
             </div>
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
@@ -590,6 +638,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <input type="password" class="form-control" name="password2">
             <input type="password" class="form-control" name="password2">
             </div>
             </div>
           </div>
           </div>
+          <div data-acl="<?=$_SESSION['acl']['extend_sender_acl'];?>" class="form-group">
+            <label class="control-label col-sm-2" for="extended_sender_acl"><?=$lang['edit']['extended_sender_acl'];?></label>
+            <div class="col-sm-10">
+            <input type="text" class="form-control" name="extended_sender_acl" value="<?=empty($ext_sender_acl) ? '' : $ext_sender_acl; ?>" placeholder="user1@example.com, user2@example.org, @example.com, ...">
+            <small class="help-block"><?=$lang['edit']['extended_sender_acl_info'];?></small>
+            </div>
+          </div>
           <div class="form-group">
           <div class="form-group">
             <div class="col-sm-offset-2 col-sm-10">
             <div class="col-sm-offset-2 col-sm-10">
               <div class="checkbox">
               <div class="checkbox">
@@ -639,6 +694,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               <div class="form-group">
               <div class="form-group">
                 <button class="btn btn-default" data-action="edit_selected" data-id="mboxratelimit" data-item="<?=htmlspecialchars($mailbox);?>" data-api-url='edit/rl-mbox' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
                 <button class="btn btn-default" data-action="edit_selected" data-id="mboxratelimit" data-item="<?=htmlspecialchars($mailbox);?>" data-api-url='edit/rl-mbox' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
               </div>
               </div>
+              <p class="help-block"><?=$lang['edit']['mbox_rl_info'];?></p>
             </div>
             </div>
           </div>
           </div>
         </form>
         </form>
@@ -681,6 +737,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               <label class="control-label col-sm-2" for="hostname"><?=$lang['add']['hostname'];?></label>
               <label class="control-label col-sm-2" for="hostname"><?=$lang['add']['hostname'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
                 <input type="text" class="form-control" name="hostname" value="<?=htmlspecialchars($result['hostname'], ENT_QUOTES, 'UTF-8');?>" required>
                 <input type="text" class="form-control" name="hostname" value="<?=htmlspecialchars($result['hostname'], ENT_QUOTES, 'UTF-8');?>" required>
+                <p class="help-block"><?=$lang['add']['relayhost_wrapped_tls_info'];?></p>
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
@@ -784,7 +841,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
-              <label class="control-label col-sm-2" for="domain"><?=$lang['edit']['kind'];?>:</label>
+              <label class="control-label col-sm-2" for="domain"><?=$lang['edit']['kind'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
                 <select name="kind" title="<?=$lang['edit']['select'];?>" required>
                 <select name="kind" title="<?=$lang['edit']['select'];?>" required>
                   <option value="location" <?=($result['kind'] == "location") ? "selected" : null;?>>Location</option>
                   <option value="location" <?=($result['kind'] == "location") ? "selected" : null;?>>Location</option>
@@ -794,7 +851,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
-              <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?>:</label>
+              <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
                 <select name="multiple_bookings_select" id="editSelectMultipleBookings" title="<?=$lang['add']['select'];?>" required>
                 <select name="multiple_bookings_select" id="editSelectMultipleBookings" title="<?=$lang['add']['select'];?>" required>
                   <option value="0" <?=($result['multiple_bookings'] == 0) ? "selected" : null;?>><?=$lang['mailbox']['booking_0'];?></option>
                   <option value="0" <?=($result['multiple_bookings'] == 0) ? "selected" : null;?>><?=$lang['mailbox']['booking_0'];?></option>
@@ -1027,7 +1084,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
-              <label class="control-label col-sm-2" for="enc1"><?=$lang['edit']['encryption'];?>:</label>
+              <label class="control-label col-sm-2" for="enc1"><?=$lang['edit']['encryption'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
                 <select id="enc1" name="enc1">
                 <select id="enc1" name="enc1">
                   <option <?=($result['enc1'] == "TLS") ? "selected" : null;?>>TLS</option>
                   <option <?=($result['enc1'] == "TLS") ? "selected" : null;?>>TLS</option>
@@ -1086,7 +1143,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
             <div class="form-group">
             <div class="form-group">
               <label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label>
               <label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
-              <input type="text" class="form-control" name="custom_params" id="custom_params" value="<?=htmlspecialchars($result['custom_params'], ENT_QUOTES, 'UTF-8');?>">
+              <input type="text" class="form-control" name="custom_params" id="custom_params" value="<?=htmlspecialchars($result['custom_params'], ENT_QUOTES, 'UTF-8');?>" placeholder="--dry --some-param=xy --other-param=yx">
+              <small class="help-block"><?=$lang['add']['custom_params_hint'];?></small>
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
@@ -1162,13 +1220,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           <form class="form-horizontal" data-id="editfilter" role="form" method="post">
           <form class="form-horizontal" data-id="editfilter" role="form" method="post">
             <input type="hidden" value="0" name="active">
             <input type="hidden" value="0" name="active">
             <div class="form-group">
             <div class="form-group">
-              <label class="control-label col-sm-2" for="script_desc"><?=$lang['edit']['sieve_desc'];?>:</label>
+              <label class="control-label col-sm-2" for="script_desc"><?=$lang['edit']['sieve_desc'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
               <input type="text" class="form-control" name="script_desc" id="script_desc" value="<?=htmlspecialchars($result['script_desc'], ENT_QUOTES, 'UTF-8');?>" required maxlength="255">
               <input type="text" class="form-control" name="script_desc" id="script_desc" value="<?=htmlspecialchars($result['script_desc'], ENT_QUOTES, 'UTF-8');?>" required maxlength="255">
               </div>
               </div>
             </div>
             </div>
             <div class="form-group">
             <div class="form-group">
-              <label class="control-label col-sm-2" for="filter_type"><?=$lang['edit']['sieve_type'];?>:</label>
+              <label class="control-label col-sm-2" for="filter_type"><?=$lang['edit']['sieve_type'];?></label>
               <div class="col-sm-10">
               <div class="col-sm-10">
                 <select id="addFilterType" name="filter_type" id="filter_type" required>
                 <select id="addFilterType" name="filter_type" id="filter_type" required>
                   <option value="prefilter" <?=($result['filter_type'] == 'prefilter') ? 'selected' : null;?>>Prefilter</option>
                   <option value="prefilter" <?=($result['filter_type'] == 'prefilter') ? 'selected' : null;?>>Prefilter</option>

+ 1 - 1
data/web/inc/ajax/dns_diagnostics.php

@@ -75,7 +75,7 @@ if (!isset($autodiscover_config['sieve'])) {
 }
 }
 
 
 // Init records array
 // Init records array
-$spf_link = '<a href="http://www.openspf.org/SPF_Record_Syntax" target="_blank">SPF Record Syntax</a><br />';
+$spf_link = '<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank">SPF Record Syntax</a><br />';
 $dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
 $dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
 
 
 $records = array();
 $records = array();

+ 19 - 1
data/web/inc/ajax/qitem_details.php

@@ -3,8 +3,9 @@ session_start();
 header("Content-Type: application/json");
 header("Content-Type: application/json");
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 if (!isset($_SESSION['mailcow_cc_role'])) {
 if (!isset($_SESSION['mailcow_cc_role'])) {
-	exit();
+  exit();
 }
 }
+
 function rrmdir($src) {
 function rrmdir($src) {
   $dir = opendir($src);
   $dir = opendir($src);
   while(false !== ( $file = readdir($dir)) ) {
   while(false !== ( $file = readdir($dir)) ) {
@@ -21,6 +22,13 @@ function rrmdir($src) {
   closedir($dir);
   closedir($dir);
   rmdir($src);
   rmdir($src);
 }
 }
+function addAddresses(&$list, $mail, $headerName) {
+  $addresses = $mail->getAddresses($headerName);
+  foreach ($addresses as $address) {
+    $list[] = array('address' => $address['address'], 'type' => $headerName);
+  }
+}
+
 if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) {
 if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) {
   $tmpdir = '/tmp/' . $_GET['id'] . '/';
   $tmpdir = '/tmp/' . $_GET['id'] . '/';
   $mailc = quarantine('details', $_GET['id']);
   $mailc = quarantine('details', $_GET['id']);
@@ -36,6 +44,16 @@ if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) {
     $html2text = new Html2Text\Html2Text();
     $html2text = new Html2Text\Html2Text();
     // Load msg to parser
     // Load msg to parser
     $mail_parser->setText($mailc['msg']);
     $mail_parser->setText($mailc['msg']);
+
+    // Get mail recipients
+    {
+      $recipientsList = array();
+      addAddresses($recipientsList, $mail_parser, 'to');
+      addAddresses($recipientsList, $mail_parser, 'cc');
+      addAddresses($recipientsList, $mail_parser, 'bcc');
+      $data['recipients'] = $recipientsList;
+    }
+
     // Get text/plain content
     // Get text/plain content
     $data['text_plain'] = $mail_parser->getMessageBody('text');
     $data['text_plain'] = $mail_parser->getMessageBody('text');
     // Get html content and convert to text
     // Get html content and convert to text

+ 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'], $_GET['token']);
+}
+
+?>

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

@@ -58,6 +58,11 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
       )
       )
     );
     );
     $mail->SMTPDebug = 3;
     $mail->SMTPDebug = 3;
+    // smtp: and smtp_enforced_tls: do not support wrapped tls, todo?
+    // change postfix map to detect wrapped tls or add a checkbox to toggle wrapped tls
+    // 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; }

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

@@ -26,6 +26,10 @@ $(window).load(function() {
   $(".overlay").hide();
   $(".overlay").hide();
 });
 });
 $(document).ready(function() {
 $(document).ready(function() {
+  $(document).on('shown.bs.modal', function(e) {
+    modal_id = $(e.relatedTarget).data('target');
+    $(modal_id).attr("aria-hidden","false");
+  });
   // TFA, CSRF, Alerts in footer.inc.php
   // TFA, CSRF, Alerts in footer.inc.php
   // Other general functions in mailcow.js
   // Other general functions in mailcow.js
   <?php
   <?php
@@ -93,6 +97,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") {

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

@@ -69,7 +69,7 @@ function bcc($_action, $_data = null, $attr = null) {
         $_SESSION['return'][] = array(
         $_SESSION['return'][] = array(
           'type' => 'danger',
           'type' => 'danger',
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
           'log' => array(__FUNCTION__, $_action, $_data, $_attr),
-          'msg' => 'bcc_must_be_email'
+          'msg' => array('bcc_must_be_email', htmlspecialchars($bcc_dest))
         );
         );
         return false;
         return false;
       }
       }

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

@@ -9,6 +9,11 @@ function valid_network($network) {
   }
   }
   return false;
   return false;
 }
 }
+
+function valid_hostname($hostname) {
+    return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
+}
+
 function fail2ban($_action, $_data = null) {
 function fail2ban($_action, $_data = null) {
   global $redis;
   global $redis;
   global $lang;
   global $lang;
@@ -188,7 +193,7 @@ function fail2ban($_action, $_data = null) {
           $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
           $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
           if (is_array($wl_array)) {
           if (is_array($wl_array)) {
             foreach ($wl_array as $wl_item) {
             foreach ($wl_array as $wl_item) {
-              if (valid_network($wl_item)) {
+              if (valid_network($wl_item) || valid_hostname($wl_item)) {
                 $redis->hSet('F2B_WHITELIST', $wl_item, 1);
                 $redis->hSet('F2B_WHITELIST', $wl_item, 1);
               }
               }
             }
             }
@@ -198,7 +203,7 @@ function fail2ban($_action, $_data = null) {
           $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
           $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
           if (is_array($bl_array)) {
           if (is_array($bl_array)) {
             foreach ($bl_array as $bl_item) {
             foreach ($bl_array as $bl_item) {
-              if (valid_network($bl_item)) {
+              if (valid_network($bl_item) || valid_hostname($bl_item)) {
                 $redis->hSet('F2B_BLACKLIST', $bl_item, 1);
                 $redis->hSet('F2B_BLACKLIST', $bl_item, 1);
               }
               }
             }
             }

+ 95 - 5
data/web/inc/functions.inc.php

@@ -1,4 +1,12 @@
 <?php
 <?php
+function isset_has_content($var) {
+  if (isset($var) && $var != "") {
+    return true;
+  }
+  else {
+    return false;
+  }
+}
 function hash_password($password) {
 function hash_password($password) {
 	$salt_str = bin2hex(openssl_random_pseudo_bytes(8));
 	$salt_str = bin2hex(openssl_random_pseudo_bytes(8));
 	return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
 	return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
@@ -248,6 +256,25 @@ function hasMailboxObjectAccess($username, $role, $object) {
   }
   }
 	return false;
 	return false;
 }
 }
+function hasAliasObjectAccess($username, $role, $object) {
+	global $pdo;
+	if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+		return false;
+	}
+	if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
+		return false;
+	}
+	if ($username == $object) {
+		return true;
+	}
+  $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
+  $stmt->execute(array(':object' => $object));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
+    return true;
+  }
+	return false;
+}
 function pem_to_der($pem_key) {
 function pem_to_der($pem_key) {
   // Need to remove BEGIN/END PUBLIC KEY
   // Need to remove BEGIN/END PUBLIC KEY
   $lines = explode("\n", trim($pem_key));
   $lines = explode("\n", trim($pem_key));
@@ -525,8 +552,8 @@ function update_sogo_static_view() {
     WHERE TABLE_NAME = 'sogo_view'");
     WHERE TABLE_NAME = 'sogo_view'");
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
   $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
   if ($num_results != 0) {
   if ($num_results != 0) {
-    $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `kind`, `multiple_bookings`)
-      SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `kind`, `multiple_bookings` from sogo_view");
+    $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
+      SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
     $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
     $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
   }
   }
   flush_memcached();
   flush_memcached();
@@ -668,7 +695,7 @@ function user_get_alias_details($username) {
   while ($row = array_shift($run)) {
   while ($row = array_shift($run)) {
     $data['aliases_also_send_as'] = $row['send_as'];
     $data['aliases_also_send_as'] = $row['send_as'];
   }
   }
-  $stmt = $pdo->prepare("SELECT IFNULL(CONCAT(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ', ', GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')), '&#10008;') AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` =  TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
+  $stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), '&#10008;'), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` =  TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
   $stmt->execute(array(':username' => $username));
   $stmt->execute(array(':username' => $username));
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
   while ($row = array_shift($run)) {
   while ($row = array_shift($run)) {
@@ -1196,6 +1223,69 @@ function admin_api($action, $data = null) {
 		'msg' => 'admin_api_modified'
 		'msg' => 'admin_api_modified'
 	);
 	);
 }
 }
+function license($action, $data = null) {
+	global $pdo;
+	global $redis;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'][] =  array(
+			'type' => 'danger',
+      'log' => array(__FUNCTION__),
+			'msg' => 'access_denied'
+		);
+		return false;
+	}
+	switch ($action) {
+		case "verify":
+      // Keep result until revalidate button is pressed or session expired
+      $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
+      $versions = $stmt->fetch(PDO::FETCH_ASSOC);
+      $post = array('guid' => $versions['version']);
+      $curl = curl_init('https://verify.mailcow.email');
+      curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+      curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
+      curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
+      $response = curl_exec($curl);
+      curl_close($curl);
+      $json_return = json_decode($response, true);
+      if ($response && $json_return) {
+        if ($json_return['response'] === "ok") {
+          $_SESSION['gal']['valid'] = "true";
+          $_SESSION['gal']['c'] = $json_return['c'];
+          $_SESSION['gal']['s'] = $json_return['s'];
+                  }
+        elseif ($json_return['response'] === "invalid") {
+          $_SESSION['gal']['valid'] = "false";
+          $_SESSION['gal']['c'] = $lang['mailbox']['no'];
+          $_SESSION['gal']['s'] = $lang['mailbox']['no'];
+        }
+      }
+      else {
+        $_SESSION['gal']['valid'] = "false";
+        $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
+        $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
+      }
+      try {
+        // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
+        $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data_log),
+          'msg' => array('redis_error', $e)
+        );
+        return false;
+      }
+      return $_SESSION['gal']['valid'];
+    break;
+    case "guid":
+      $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
+      $versions = $stmt->fetch(PDO::FETCH_ASSOC);
+      return $versions['version'];
+    break;
+  }
+}
 function rspamd_ui($action, $data = null) {
 function rspamd_ui($action, $data = null) {
 	global $lang;
 	global $lang;
 	if ($_SESSION['mailcow_cc_role'] != "admin") {
 	if ($_SESSION['mailcow_cc_role'] != "admin") {
@@ -1477,7 +1567,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 +1584,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;
 }
 }

+ 245 - 60
data/web/inc/functions.mailbox.inc.php

@@ -326,9 +326,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $description  = $_data['description'];
           $description  = $_data['description'];
           $aliases			= $_data['aliases'];
           $aliases			= $_data['aliases'];
           $mailboxes    = $_data['mailboxes'];
           $mailboxes    = $_data['mailboxes'];
+          $defquota			= $_data['defquota'];
           $maxquota			= $_data['maxquota'];
           $maxquota			= $_data['maxquota'];
           $restart_sogo = $_data['restart_sogo'];
           $restart_sogo = $_data['restart_sogo'];
           $quota				= $_data['quota'];
           $quota				= $_data['quota'];
+          if ($defquota > $maxquota) {
+            $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'mailbox_defquota_exceeds_mailbox_maxquota'
+            );
+            return false;
+          }
           if ($maxquota > $quota) {
           if ($maxquota > $quota) {
             $_SESSION['return'][] = array(
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'type' => 'danger',
@@ -337,6 +346,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             );
             return false;
             return false;
           }
           }
+          if ($defquota == "0" || empty($defquota)) {
+            $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'defquota_empty'
+            );
+            return false;
+          }
           if ($maxquota == "0" || empty($maxquota)) {
           if ($maxquota == "0" || empty($maxquota)) {
             $_SESSION['return'][] = array(
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'type' => 'danger',
@@ -392,13 +409,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             );
             return false;
             return false;
           }
           }
-          $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_all_recipients`)
-            VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, :backupmx, :gal, :active, :relay_all_recipients)");
+          $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain");
+          $stmt->execute(array(
+            ':domain' => '%@' . $domain
+          ));
+          $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_all_recipients`)
+            VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_all_recipients)");
           $stmt->execute(array(
           $stmt->execute(array(
             ':domain' => $domain,
             ':domain' => $domain,
             ':description' => $description,
             ':description' => $description,
             ':aliases' => $aliases,
             ':aliases' => $aliases,
             ':mailboxes' => $mailboxes,
             ':mailboxes' => $mailboxes,
+            ':defquota' => $defquota,
             ':maxquota' => $maxquota,
             ':maxquota' => $maxquota,
             ':quota' => $quota,
             ':quota' => $quota,
             ':backupmx' => $backupmx,
             ':backupmx' => $backupmx,
@@ -561,7 +583,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 +595,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 +607,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 +615,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 +623,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)");
@@ -692,6 +714,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               );
               continue;
               continue;
             }
             }
+            $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+              WHERE `domain`= :target_domain AND `backupmx` = '1'");
+            $stmt->execute(array(':target_domain' => $target_domain));
+            $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+            if ($num_results == 1) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => array('targetd_relay_domain', htmlspecialchars($target_domain))
+              );
+              continue;
+            }
             $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
             $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
               UNION
               UNION
               SELECT `domain` FROM `domain` WHERE `domain`= :alias_domain_in_domain");
               SELECT `domain` FROM `domain` WHERE `domain`= :alias_domain_in_domain");
@@ -705,6 +739,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               );
               continue;
               continue;
             }
             }
+            $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain");
+            $stmt->execute(array(
+              ':domain' => '%@' . $domain
+            ));
             $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`)
             $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`)
               VALUES (:alias_domain, :target_domain, :active)");
               VALUES (:alias_domain, :target_domain, :active)");
             $stmt->execute(array(
             $stmt->execute(array(
@@ -756,7 +794,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $password     = $_data['password'];
           $password     = $_data['password'];
           $password2    = $_data['password2'];
           $password2    = $_data['password2'];
           $name         = ltrim(rtrim($_data['name'], '>'), '<');
           $name         = ltrim(rtrim($_data['name'], '>'), '<');
-          $quota_m			= filter_var($_data['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
+          $quota_m			= intval($_data['quota']);
+          if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'unlimited_quota_acl'
+            );
+            return false;
+          }
           if (empty($name)) {
           if (empty($name)) {
             $name = $local_part;
             $name = $local_part;
           }
           }
@@ -844,14 +890,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             );
             return false;
             return false;
           }
           }
-          if (!is_numeric($quota_m) || $quota_m == "0") {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-              'msg' => 'quota_not_0_not_numeric'
-            );
-            return false;
-          }
           if (!empty($password) && !empty($password2)) {
           if (!empty($password) && !empty($password2)) {
             if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
             if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
@@ -1695,25 +1733,27 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 );
                 );
                 continue;
                 continue;
               }
               }
-              $stmt = $pdo->prepare("SELECT `address` FROM `alias`
-                WHERE `address`= :address OR `address` IN (
-                  SELECT `username` FROM `mailbox`, `alias_domain`
-                    WHERE (
-                      `alias_domain`.`alias_domain` = :address_d
-                        AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))");
-              $stmt->execute(array(
-                ':address' => $address,
-                ':address_l' => $local_part,
-                ':address_d' => $domain
-              ));
-              $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-              if ($num_results != 0) {
-                $_SESSION['return'][] = array(
-                  'type' => 'danger',
-                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                  'msg' => array('is_alias_or_mailbox', htmlspecialchars($address))
-                );
-                continue;
+              if (strtolower($is_now['address']) != strtolower($address)) {
+                $stmt = $pdo->prepare("SELECT `address` FROM `alias`
+                  WHERE `address`= :address OR `address` IN (
+                    SELECT `username` FROM `mailbox`, `alias_domain`
+                      WHERE (
+                        `alias_domain`.`alias_domain` = :address_d
+                          AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))");
+                $stmt->execute(array(
+                  ':address' => $address,
+                  ':address_l' => $local_part,
+                  ':address_d' => $domain
+                ));
+                $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+                if ($num_results != 0) {
+                  $_SESSION['return'][] = array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                    'msg' => array('is_alias_or_mailbox', htmlspecialchars($address))
+                  );
+                  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)");
@@ -1773,6 +1813,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                   unset($gotos[$i]);
                   unset($gotos[$i]);
                   continue;
                   continue;
                 }
                 }
+                // Delete from sender_acl to prevent duplicates
+                $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE
+                  `logged_in_as` = :goto AND
+                  `send_as` = :address");
+                $stmt->execute(array(
+                  ':goto' => $goto,
+                  ':address' => $address
+                ));
               }
               }
               $gotos = array_filter($gotos);
               $gotos = array_filter($gotos);
               $goto = implode(",", $gotos);
               $goto = implode(",", $gotos);
@@ -1859,6 +1907,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $relayhost            = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost'];
                 $relayhost            = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost'];
                 $aliases              = (!empty($_data['aliases'])) ? $_data['aliases'] : $is_now['max_num_aliases_for_domain'];
                 $aliases              = (!empty($_data['aliases'])) ? $_data['aliases'] : $is_now['max_num_aliases_for_domain'];
                 $mailboxes            = (isset($_data['mailboxes']) && $_data['mailboxes'] != '') ? intval($_data['mailboxes']) : $is_now['max_num_mboxes_for_domain'];
                 $mailboxes            = (isset($_data['mailboxes']) && $_data['mailboxes'] != '') ? intval($_data['mailboxes']) : $is_now['max_num_mboxes_for_domain'];
+                $defquota             = (isset($_data['defquota']) && $_data['defquota'] != '') ? intval($_data['defquota']) : ($is_now['def_quota_for_mbox'] / 1048576);
                 $maxquota             = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
                 $maxquota             = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
                 $quota                = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
                 $quota                = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
                 $description          = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
                 $description          = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
@@ -1890,6 +1939,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                   )");
                   )");
               $stmt->execute(array(':domain' => $domain));
               $stmt->execute(array(':domain' => $domain));
               $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
               $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
+              if ($defquota > $maxquota) {
+                $_SESSION['return'][] = array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                    'msg' => 'mailbox_defquota_exceeds_mailbox_maxquota'
+                );
+                continue;
+              }
+              if ($defquota == "0" || empty($defquota)) {
+                $_SESSION['return'][] = array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                    'msg' => 'defquota_empty'
+                );
+                continue;
+              }
               if ($maxquota > $quota) {
               if ($maxquota > $quota) {
                 $_SESSION['return'][] = array(
                 $_SESSION['return'][] = array(
                   'type' => 'danger',
                   'type' => 'danger',
@@ -1944,6 +2009,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `gal` = :gal,
               `gal` = :gal,
               `active` = :active,
               `active` = :active,
               `quota` = :quota,
               `quota` = :quota,
+              `defquota` = :defquota,
               `maxquota` = :maxquota,
               `maxquota` = :maxquota,
               `relayhost` = :relayhost,
               `relayhost` = :relayhost,
               `mailboxes` = :mailboxes,
               `mailboxes` = :mailboxes,
@@ -1956,6 +2022,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ':gal' => $gal,
                 ':gal' => $gal,
                 ':active' => $active,
                 ':active' => $active,
                 ':quota' => $quota,
                 ':quota' => $quota,
+                ':defquota' => $defquota,
                 ':maxquota' => $maxquota,
                 ':maxquota' => $maxquota,
                 ':relayhost' => $relayhost,
                 ':relayhost' => $relayhost,
                 ':mailboxes' => $mailboxes,
                 ':mailboxes' => $mailboxes,
@@ -1993,9 +2060,9 @@ 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']);
+              (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
               $name       = (!empty($_data['name'])) ? ltrim(rtrim($_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_b    = $quota_m * 1048576;
               $quota_b    = $quota_m * 1048576;
               $password   = (!empty($_data['password'])) ? $_data['password'] : null;
               $password   = (!empty($_data['password'])) ? $_data['password'] : null;
               $password2  = (!empty($_data['password2'])) ? $_data['password2'] : null; 
               $password2  = (!empty($_data['password2'])) ? $_data['password2'] : null; 
@@ -2008,6 +2075,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               );
               continue;
               continue;
             }
             }
+            // if already 0 == ok
+            if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'unlimited_quota_acl'
+              );
+              return false;
+            }
             $stmt = $pdo->prepare("SELECT `quota`, `maxquota`
             $stmt = $pdo->prepare("SELECT `quota`, `maxquota`
               FROM `domain`
               FROM `domain`
                 WHERE `domain` = :domain");
                 WHERE `domain` = :domain");
@@ -2021,14 +2097,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               );
               continue;
               continue;
             }
             }
-            if (!is_numeric($quota_m) || $quota_m == "0") {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                'msg' => array('quota_not_0_not_numeric', htmlspecialchars($quota_m))
-              );
-              continue;
-            }
             if ($quota_m > $DomainData['maxquota']) {
             if ($quota_m > $DomainData['maxquota']) {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'type' => 'danger',
@@ -2045,6 +2113,75 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               );
               continue;
               continue;
             }
             }
+            $extra_acls = array();
+            if (isset($_data['extended_sender_acl'])) {
+              if (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1" ) {
+                $_SESSION['return'][] = array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                  'msg' => 'access_denied'
+                );
+                return false;
+              }
+              $extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
+              foreach ($extra_acls as $i => &$extra_acl) {
+                if (empty($extra_acl)) {
+                  continue;
+                }
+                if (substr($extra_acl, 0, 1) === "@") {
+                  $extra_acl = ltrim($extra_acl, '@');
+                }
+                if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
+                  $_SESSION['return'][] = array(
+                    'type' => 'danger',
+                    'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                    'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
+                  );
+                  unset($extra_acls[$i]);
+                  continue;
+                }
+                $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
+                if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
+                  $extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
+                  if (in_array($extra_acl_domain, $domains)) {
+                    $_SESSION['return'][] = array(
+                      'type' => 'danger',
+                      'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                      'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
+                    );
+                    unset($extra_acls[$i]);
+                    continue;
+                  }
+                }
+                else {
+                  if (in_array($extra_acl, $domains)) {
+                    $_SESSION['return'][] = array(
+                      'type' => 'danger',
+                      'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                      'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
+                    );
+                    unset($extra_acls[$i]);
+                    continue;
+                  }
+                  $extra_acl = '@' . $extra_acl;
+                }
+              }
+              $extra_acls = array_filter($extra_acls);
+              $extra_acls = array_values($extra_acls);
+              $extra_acls = array_unique($extra_acls);
+              $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
+              $stmt->execute(array(
+                ':username' => $username
+              ));
+              foreach ($extra_acls as $sender_acl_external) {
+                $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
+                  VALUES (:sender_acl, :username, 1)");
+                $stmt->execute(array(
+                  ':sender_acl' => $sender_acl_external,
+                  ':username' => $username
+                ));
+              }
+            }
             if (isset($_data['sender_acl'])) {
             if (isset($_data['sender_acl'])) {
               // Get sender_acl items set by admin
               // Get sender_acl items set by admin
               $sender_acl_admin = array_merge(
               $sender_acl_admin = array_merge(
@@ -2116,9 +2253,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                     unset($sender_acl_domain_admin[$key]);
                     unset($sender_acl_domain_admin[$key]);
                     continue;
                     continue;
                   }
                   }
-                  // Check if user has mailbox access (if object is email)
+                  // Check if user has alias access (if object is email)
                   if (filter_var($val, FILTER_VALIDATE_EMAIL)) {
                   if (filter_var($val, FILTER_VALIDATE_EMAIL)) {
-                    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) {
+                    if (!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) {
                       $_SESSION['return'][] = array(
                       $_SESSION['return'][] = array(
                         'type' => 'danger',
                         'type' => 'danger',
                         'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                         'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2133,15 +2270,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
                 $sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
                 // If merged array still contains "*", set it as only value
                 // If merged array still contains "*", set it as only value
                 !in_array('*', $sender_acl_merged) ?: $sender_acl_merged = array('*');
                 !in_array('*', $sender_acl_merged) ?: $sender_acl_merged = array('*');
-                $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+                $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username");
                 $stmt->execute(array(
                 $stmt->execute(array(
                   ':username' => $username
                   ':username' => $username
                 ));
                 ));
+                $fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
                 foreach ($sender_acl_merged as $sender_acl) {
                 foreach ($sender_acl_merged as $sender_acl) {
                   $domain = ltrim($sender_acl, '@');
                   $domain = ltrim($sender_acl, '@');
                   if (is_valid_domain_name($domain)) {
                   if (is_valid_domain_name($domain)) {
                     $sender_acl = '@' . $domain;
                     $sender_acl = '@' . $domain;
                   }
                   }
+                  // Don't add if allowed by alias
+                  if (in_array($sender_acl, $fixed_sender_aliases)) {
+                    continue;
+                  }
                   $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
                   $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
                     VALUES (:sender_acl, :username)");
                     VALUES (:sender_acl, :username)");
                   $stmt->execute(array(
                   $stmt->execute(array(
@@ -2151,7 +2293,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 }
                 }
               }
               }
               else {
               else {
-                $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+                $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username");
                 $stmt->execute(array(
                 $stmt->execute(array(
                   ':username' => $username
                   ':username' => $username
                 ));
                 ));
@@ -2306,6 +2448,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $data['sender_acl_addresses']['rw']             = array();
           $data['sender_acl_addresses']['rw']             = array();
           $data['sender_acl_addresses']['selectable']     = array();
           $data['sender_acl_addresses']['selectable']     = array();
           $data['fixed_sender_aliases']                   = array();
           $data['fixed_sender_aliases']                   = array();
+          $data['external_sender_aliases']                = array();
           // Fixed addresses
           // Fixed addresses
           $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
           $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
           $stmt->execute(array(':goto' => '(^|,)'.$_data.'($|,)'));
           $stmt->execute(array(':goto' => '(^|,)'.$_data.'($|,)'));
@@ -2323,9 +2466,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $data['fixed_sender_aliases'][] = $row['alias_domain_alias'];
               $data['fixed_sender_aliases'][] = $row['alias_domain_alias'];
             }
             }
           }
           }
+          // External addresses
+          $stmt = $pdo->prepare("SELECT `send_as` as `send_as_external` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '1'");
+          $stmt->execute(array(':logged_in_as' => $_data));
+          $exernal_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+          while ($row = array_shift($exernal_rows)) {
+            if (!empty($row['send_as_external'])) {
+              $data['external_sender_aliases'][] = $row['send_as_external'];
+            }
+          }
           // Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
           // Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
           // Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
           // Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
-          $stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND (`send_as` LIKE '@%' OR `send_as` = '*')");
+          $stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` LIKE '@%' OR `send_as` = '*')");
           $stmt->execute(array(':logged_in_as' => $_data));
           $stmt->execute(array(':logged_in_as' => $_data));
           $domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           $domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           while ($domain_row = array_shift($domain_rows)) {
           while ($domain_row = array_shift($domain_rows)) {
@@ -2344,15 +2496,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
               $data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
             }
             }
           }
           }
-          $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')");
+          $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')");
           $stmt->execute(array(':logged_in_as' => $_data));
           $stmt->execute(array(':logged_in_as' => $_data));
           $address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           $address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           while ($address_row = array_shift($address_rows)) {
           while ($address_row = array_shift($address_rows)) {
-            if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
+            if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
               $data['sender_acl_addresses']['ro'][] = $address_row['send_as'];
               $data['sender_acl_addresses']['ro'][] = $address_row['send_as'];
               continue;
               continue;
             }
             }
-            if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
+            if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
               $data['sender_acl_addresses']['rw'][] = $address_row['send_as'];
               $data['sender_acl_addresses']['rw'][] = $address_row['send_as'];
               continue;
               continue;
             }
             }
@@ -2361,12 +2513,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             WHERE `domain` NOT IN (
             WHERE `domain` NOT IN (
               SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` 
               SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` 
                 WHERE `logged_in_as` = :logged_in_as1
                 WHERE `logged_in_as` = :logged_in_as1
+                  AND `external` = '0'
                   AND `send_as` LIKE '@%')
                   AND `send_as` LIKE '@%')
             UNION
             UNION
             SELECT '*' FROM `domain`
             SELECT '*' FROM `domain`
               WHERE '*' NOT IN (
               WHERE '*' NOT IN (
                 SELECT `send_as` FROM `sender_acl`  
                 SELECT `send_as` FROM `sender_acl`  
                   WHERE `logged_in_as` = :logged_in_as2
                   WHERE `logged_in_as` = :logged_in_as2
+                    AND `external` = '0'
               )");
               )");
           $stmt->execute(array(
           $stmt->execute(array(
             ':logged_in_as1' => $_data,
             ':logged_in_as1' => $_data,
@@ -2388,6 +2542,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               AND `address` NOT IN (
               AND `address` NOT IN (
                 SELECT `send_as` FROM `sender_acl` 
                 SELECT `send_as` FROM `sender_acl` 
                   WHERE `logged_in_as` = :logged_in_as
                   WHERE `logged_in_as` = :logged_in_as
+                    AND `external` = '0'
                     AND `send_as` NOT LIKE '@%')");
                     AND `send_as` NOT LIKE '@%')");
           $stmt->execute(array(
           $stmt->execute(array(
             ':logged_in_as' => $_data,
             ':logged_in_as' => $_data,
@@ -2395,7 +2550,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           ));
           ));
           $rows_mbox = $stmt->fetchAll(PDO::FETCH_ASSOC);
           $rows_mbox = $stmt->fetchAll(PDO::FETCH_ASSOC);
           while ($row = array_shift($rows_mbox)) {
           while ($row = array_shift($rows_mbox)) {
-            if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
+            // Aliases are not selectable
+            if (in_array($row['address'], $data['fixed_sender_aliases'])) {
+              continue;
+            }
+            if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
               $data['sender_acl_addresses']['selectable'][] = $row['address'];
               $data['sender_acl_addresses']['selectable'][] = $row['address'];
             }
             }
           }
           }
@@ -2852,7 +3011,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':aliasdomain' => $_data,
             ':aliasdomain' => $_data,
           ));
           ));
           $row = $stmt->fetch(PDO::FETCH_ASSOC);
           $row = $stmt->fetch(PDO::FETCH_ASSOC);
+          $stmt = $pdo->prepare("SELECT `backupmx` FROM `domain` WHERE `domain` = :target_domain");
+          $stmt->execute(array(
+            ':target_domain' => $row['target_domain']
+          ));
+          $row_parent = $stmt->fetch(PDO::FETCH_ASSOC);
           $aliasdomaindata['alias_domain'] = $row['alias_domain'];
           $aliasdomaindata['alias_domain'] = $row['alias_domain'];
+          $aliasdomaindata['parent_is_backupmx'] = $row_parent['backupmx'];
           $aliasdomaindata['target_domain'] = $row['target_domain'];
           $aliasdomaindata['target_domain'] = $row['target_domain'];
           $aliasdomaindata['active'] = $row['active'];
           $aliasdomaindata['active'] = $row['active'];
           $aliasdomaindata['rl'] = $rl;
           $aliasdomaindata['rl'] = $rl;
@@ -2904,6 +3069,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `description`,
               `description`,
               `aliases`,
               `aliases`,
               `mailboxes`, 
               `mailboxes`, 
+              `defquota`,
               `maxquota`,
               `maxquota`,
               `quota`,
               `quota`,
               `relayhost`,
               `relayhost`,
@@ -2935,6 +3101,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
           if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
             $domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
             $domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
           }
           }
+          $domaindata['def_new_mailbox_quota'] = $domaindata['max_new_mailbox_quota'];
+          if ($domaindata['def_new_mailbox_quota'] > ($row['defquota'] * 1048576)) {
+            $domaindata['def_new_mailbox_quota'] = ($row['defquota'] * 1048576);
+          }
           $domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use'];
           $domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use'];
           $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
           $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
           $domaindata['mboxes_left'] = $row['mailboxes']	- $MailboxDataDomain['count'];
           $domaindata['mboxes_left'] = $row['mailboxes']	- $MailboxDataDomain['count'];
@@ -2942,6 +3112,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $domaindata['description'] = $row['description'];
           $domaindata['description'] = $row['description'];
           $domaindata['max_num_aliases_for_domain'] = $row['aliases'];
           $domaindata['max_num_aliases_for_domain'] = $row['aliases'];
           $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
           $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
+          $domaindata['def_quota_for_mbox'] = $row['defquota'] * 1048576;
           $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576;
           $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576;
           $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576;
           $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576;
           $domaindata['relayhost'] = $row['relayhost'];
           $domaindata['relayhost'] = $row['relayhost'];
@@ -3006,7 +3177,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
             $mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
           }
           }
           $mailboxdata['username'] = $row['username'];
           $mailboxdata['username'] = $row['username'];
-          $mailboxdata['rl'] = $rl;
+          if (!empty($rl)) {
+            $mailboxdata['rl'] = $rl;
+            $mailboxdata['rl_scope'] = 'mailbox';
+          }
+          else {
+            $mailboxdata['rl'] = ratelimit('get', 'domain', $row['domain']);
+            $mailboxdata['rl_scope'] = 'domain';
+          }
           $mailboxdata['is_relayed'] = $row['backupmx'];
           $mailboxdata['is_relayed'] = $row['backupmx'];
           $mailboxdata['name'] = $row['name'];
           $mailboxdata['name'] = $row['name'];
           $mailboxdata['active'] = $row['active'];
           $mailboxdata['active'] = $row['active'];
@@ -3016,10 +3194,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['quota'] = $row['quota'];
           $mailboxdata['quota'] = $row['quota'];
           $mailboxdata['attributes'] = json_decode($row['attributes'], true);
           $mailboxdata['attributes'] = json_decode($row['attributes'], true);
           $mailboxdata['quota_used'] = intval($row['bytes']);
           $mailboxdata['quota_used'] = intval($row['bytes']);
-          $mailboxdata['percent_in_use'] = round((intval($row['bytes']) / intval($row['quota'])) * 100);
+          $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
           $mailboxdata['messages'] = $row['messages'];
           $mailboxdata['messages'] = $row['messages'];
           $mailboxdata['spam_aliases'] = $SpamaliasUsage['sa_count'];
           $mailboxdata['spam_aliases'] = $SpamaliasUsage['sa_count'];
-          if ($mailboxdata['percent_in_use'] >= 90) {
+          if ($mailboxdata['percent_in_use'] === '- ') {
+            $mailboxdata['percent_class'] = "info";
+          }
+          elseif ($mailboxdata['percent_in_use'] >= 90) {
             $mailboxdata['percent_class'] = "danger";
             $mailboxdata['percent_class'] = "danger";
           }
           }
           elseif ($mailboxdata['percent_in_use'] >= 75) {
           elseif ($mailboxdata['percent_in_use'] >= 75) {
@@ -3317,7 +3498,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                'msg' => 'domain_not_empty'
+                'msg' => array('domain_not_empty', $domain)
               );
               );
               continue;
               continue;
             }
             }
@@ -3411,6 +3592,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt->execute(array(
             $stmt->execute(array(
               ':id' => $id
               ':id' => $id
             ));
             ));
+            $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `send_as` = :alias_address");
+            $stmt->execute(array(
+              ':alias_address' => $alias_data['address']
+            ));
             $_SESSION['return'][] = array(
             $_SESSION['return'][] = array(
               'type' => 'success',
               'type' => 'success',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3525,7 +3710,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);
@@ -3587,7 +3772,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt->execute(array(
             $stmt->execute(array(
               ':username' => $username
               ':username' => $username
             ));
             ));
-            $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
+            $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username");
             $stmt->execute(array(
             $stmt->execute(array(
               ':username' => $username
               ':username' => $username
             ));
             ));
@@ -3714,7 +3899,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();
   }
   }
 }
 }

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