git subrepo clone https://github.com/mailcow/mailcow-dockerized.git mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "a832becb"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "a832becb"
git-subrepo: version:  "0.4.3"
  origin:   "???"
  commit:   "???"
Change-Id: If5be2d621a211e164c9b6577adaa7884449f16b5
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/backup_and_restore.sh b/mailcow/src/mailcow-dockerized/helper-scripts/backup_and_restore.sh
new file mode 100755
index 0000000..82002d7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/backup_and_restore.sh
@@ -0,0 +1,344 @@
+#!/usr/bin/env bash
+
+DEBIAN_DOCKER_IMAGE="debian:buster-slim"
+
+if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then
+  BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}"
+fi
+
+if [[ ! ${1} =~ (backup|restore) ]]; then
+  echo "First parameter needs to be 'backup' or 'restore'"
+  exit 1
+fi
+
+if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days) ]]; then
+  echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql', 'all' or '--delete-days'"
+  exit 1
+fi
+
+if [[ -z ${BACKUP_LOCATION} ]]; then
+  while [[ -z ${BACKUP_LOCATION} ]]; do
+    read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION
+  done
+fi
+
+if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then
+  echo "Backup directory needs to be given as absolute path (starting with /)."
+  exit 1
+fi
+
+if [[ -f ${BACKUP_LOCATION} ]]; then
+  echo "${BACKUP_LOCATION} is a file!"
+  exit 1
+fi
+
+if [[ ! -d ${BACKUP_LOCATION} ]]; then
+  echo "${BACKUP_LOCATION} is not a directory"
+  read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION
+  if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then
+    exit 1
+  else
+    mkdir -p ${BACKUP_LOCATION}
+    chmod 755 ${BACKUP_LOCATION}
+  fi
+else
+  if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a ${BACKUP_LOCATION}) | grep -oE '[0-9][0-9][5-7]') ]]; then
+    echo "${BACKUP_LOCATION} is not write-able for others, that's required for a backup."
+    exit 1
+  fi
+fi
+
+BACKUP_LOCATION=$(echo ${BACKUP_LOCATION} | sed 's#/$##')
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml
+ENV_FILE=${SCRIPT_DIR}/../.env
+
+if [ ! -f ${COMPOSE_FILE} ]; then
+  echo "Compose file not found"
+  exit 1
+fi
+
+if [ ! -f ${ENV_FILE} ]; then
+  echo "Environment file not found"
+  exit 1
+fi
+
+echo "Using ${BACKUP_LOCATION} as backup/restore location."
+echo
+
+source ${SCRIPT_DIR}/../mailcow.conf
+
+if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then
+  echo "Could not determine compose project name"
+  exit 1
+else
+  echo "Found project name ${COMPOSE_PROJECT_NAME}"
+  CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
+fi
+
+function backup() {
+  DATE=$(date +"%Y-%m-%d-%H-%M-%S")
+  mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
+  chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
+  cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
+  while (( "$#" )); do
+    case "$1" in
+    vmail|all)
+      docker run --name mailcow-backup --rm \
+        -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail:ro,z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_vmail.tar.gz /vmail
+      ;;&
+    crypt|all)
+      docker run --name mailcow-backup --rm \
+        -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt:ro,z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_crypt.tar.gz /crypt
+      ;;&
+    redis|all)
+      docker exec $(docker ps -qf name=redis-mailcow) redis-cli save
+      docker run --name mailcow-backup --rm \
+        -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis:ro,z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_redis.tar.gz /redis
+      ;;&
+    rspamd|all)
+      docker run --name mailcow-backup --rm \
+        -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd:ro,z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd
+      ;;&
+    postfix|all)
+      docker run --name mailcow-backup --rm \
+        -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix:ro,z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable" -Pcvpf /backup/backup_postfix.tar.gz /postfix
+      ;;&
+    mysql|all)
+      SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
+      if [[ -z "${SQLIMAGE}" ]]; then
+        echo "Could not determine SQL image version, skipping backup..."
+        shift
+        continue
+      else
+        echo "Using SQL image ${SQLIMAGE}, starting..."
+        docker run --name mailcow-backup --rm \
+          --network $(docker network ls -qf name=${CMPS_PRJ}_mailcow-network) \
+          -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro,z \
+          --entrypoint= \
+          --sysctl net.ipv6.conf.all.disable_ipv6=1 \
+          -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \
+          ${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \
+          mariabackup --prepare --target-dir=/backup_mariadb ; \
+          chown -R 999:999 /backup_mariadb ; \
+          /bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;"
+      fi
+      ;;&
+    --delete-days)
+      shift
+      if [[ "${1}" =~ ^[0-9]+$ ]]; then
+        find ${BACKUP_LOCATION}/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \;
+      else
+        echo "Parameter of --delete-days is not a number."
+      fi
+      ;;
+    esac
+    shift
+  done
+}
+
+function restore() {
+  echo
+  echo "Stopping watchdog-mailcow..."
+  docker stop $(docker ps -qf name=watchdog-mailcow)
+  echo
+  RESTORE_LOCATION="${1}"
+  shift
+  while (( "$#" )); do
+    case "$1" in
+    vmail)
+      docker stop $(docker ps -qf name=dovecot-mailcow)
+      docker run -it --name mailcow-backup --rm \
+        -v ${RESTORE_LOCATION}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail:z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_vmail.tar.gz
+      docker start $(docker ps -aqf name=dovecot-mailcow)
+      echo
+      echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:"
+      echo
+      echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'"
+      echo
+      read -p "Force a resync now? [y|N] " FORCE_RESYNC
+      if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then
+        docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'
+      else
+        echo "OK, skipped."
+      fi
+      ;;
+    redis)
+      docker stop $(docker ps -qf name=redis-mailcow)
+      docker run -it --name mailcow-backup --rm \
+        -v ${RESTORE_LOCATION}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis:z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_redis.tar.gz
+      docker start $(docker ps -aqf name=redis-mailcow)
+      ;;
+    crypt)
+      docker stop $(docker ps -qf name=dovecot-mailcow)
+      docker run -it --name mailcow-backup --rm \
+        -v ${RESTORE_LOCATION}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt:z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_crypt.tar.gz
+      docker start $(docker ps -aqf name=dovecot-mailcow)
+      ;;
+    rspamd)
+      docker stop $(docker ps -qf name=rspamd-mailcow)
+      docker run -it --name mailcow-backup --rm \
+        -v ${RESTORE_LOCATION}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd:z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_rspamd.tar.gz
+      docker start $(docker ps -aqf name=rspamd-mailcow)
+      ;;
+    postfix)
+      docker stop $(docker ps -qf name=postfix-mailcow)
+      docker run -it --name mailcow-backup --rm \
+        -v ${RESTORE_LOCATION}:/backup:z \
+        -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix:z \
+        ${DEBIAN_DOCKER_IMAGE} /bin/tar -Pxvzf /backup/backup_postfix.tar.gz
+      docker start $(docker ps -aqf name=postfix-mailcow)
+      ;;
+    mysql|mariadb)
+      SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
+      if [[ -z "${SQLIMAGE}" ]]; then
+        echo "Could not determine SQL image version, skipping restore..."
+        shift
+        continue
+      elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then
+        echo "Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore."
+        echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process."
+        shift
+        continue
+      else
+        read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW
+        if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then
+          echo "OK, skipped."
+          shift
+          continue
+        else
+          echo "Stopping mailcow..."
+          docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down
+        fi
+        #docker stop $(docker ps -qf name=mysql-mailcow)
+        if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then
+        docker run --name mailcow-backup --rm \
+          -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:rw,z \
+          --entrypoint= \
+          -v ${RESTORE_LOCATION}/mysql:/backup:z \
+          ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/"
+        elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then
+        docker run \
+          -it --name mailcow-backup --rm \
+          -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:z \
+          --entrypoint= \
+          -u mysql \
+          -v ${RESTORE_LOCATION}:/backup:z \
+          ${SQLIMAGE} /bin/sh -c "mysqld --skip-grant-tables & \
+          until mysqladmin ping; do sleep 3; done && \
+          echo Restoring... && \
+          gunzip < backup/backup_mysql.gz | mysql -uroot && \
+          mysql -uroot -e SHUTDOWN;"
+        elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then
+        docker run --name mailcow-backup --rm \
+          -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/backup_mariadb/:rw,z \
+          --entrypoint= \
+          -v ${RESTORE_LOCATION}:/backup:z \
+          ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \
+            /bin/rm -rf /backup_mariadb/* ; \
+            /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz"
+        fi
+        echo "Modifying mailcow.conf..."
+        source ${RESTORE_LOCATION}/mailcow.conf
+        sed -i --follow-symlinks "/DBNAME/c\DBNAME=${DBNAME}" ${SCRIPT_DIR}/../mailcow.conf
+        sed -i --follow-symlinks "/DBUSER/c\DBUSER=${DBUSER}" ${SCRIPT_DIR}/../mailcow.conf
+        sed -i --follow-symlinks "/DBPASS/c\DBPASS=${DBPASS}" ${SCRIPT_DIR}/../mailcow.conf
+        sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf
+        source ${SCRIPT_DIR}/../mailcow.conf
+        echo "Starting mailcow..."
+        docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d
+        #docker start $(docker ps -aqf name=mysql-mailcow)
+      fi
+      ;;
+    esac
+    shift
+  done
+  echo
+  echo "Starting watchdog-mailcow..."
+  docker start $(docker ps -aqf name=watchdog-mailcow)
+}
+
+if [[ ${1} == "backup" ]]; then
+  backup ${@,,}
+elif [[ ${1} == "restore" ]]; then
+  i=1
+  declare -A FOLDER_SELECTION
+  if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then
+    echo "Selected backup location has no subfolders"
+    exit 1
+  fi
+  for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do
+    echo "[ ${i} ] - ${folder}"
+    FOLDER_SELECTION[${i}]="${folder}"
+    ((i++))
+  done
+  echo
+  input_sel=0
+  while [[ ${input_sel} -lt 1 ||  ${input_sel} -gt ${i} ]]; do
+    read -p "Select a restore point: " input_sel
+  done
+  i=1
+  echo
+  declare -A FILE_SELECTION
+  RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}"
+  if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then
+    echo "No datasets found"
+    exit 1
+  fi
+
+  echo "[ 0 ] - all"
+  # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz
+  FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//')
+  for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do
+    if [[ ${file} =~ vmail ]]; then
+      echo "[ ${i} ] - Mail directory (/var/vmail)"
+      FILE_SELECTION[${i}]="vmail"
+      ((i++))
+    elif [[ ${file} =~ crypt ]]; then
+      echo "[ ${i} ] - Crypt data"
+      FILE_SELECTION[${i}]="crypt"
+      ((i++))
+    elif [[ ${file} =~ redis ]]; then
+      echo "[ ${i} ] - Redis DB"
+      FILE_SELECTION[${i}]="redis"
+      ((i++))
+    elif [[ ${file} =~ rspamd ]]; then
+      echo "[ ${i} ] - Rspamd data"
+      FILE_SELECTION[${i}]="rspamd"
+      ((i++))
+    elif [[ ${file} =~ postfix ]]; then
+      echo "[ ${i} ] - Postfix data"
+      FILE_SELECTION[${i}]="postfix"
+      ((i++))
+    elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then
+      echo "[ ${i} ] - SQL DB"
+      FILE_SELECTION[${i}]="mysql"
+      ((i++))
+    fi
+  done
+  echo
+  input_sel=-1
+  while [[ ${input_sel} -lt 0 ||  ${input_sel} -gt ${i} ]]; do
+    read -p "Select a dataset to restore: " input_sel
+  done
+  echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..."
+  restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]}
+fi
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/check_translations.rb b/mailcow/src/mailcow-dockerized/helper-scripts/check_translations.rb
new file mode 100755
index 0000000..af4da1c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/check_translations.rb
@@ -0,0 +1,34 @@
+#!/usr/bin/env ruby
+
+MASTER="en"
+
+DIR = "#{__dir__}/.."
+
+keys = %x[sed -r 's/.*(\\['.*'\\]\\['.*'\\]).*/\\1/g' #{DIR}/data/web/lang/lang.#{MASTER}.php | grep  '^\\\[' | sed 's/\\[/\\\\[/g' | sed 's/\\]/\\\\]/g'|sort | uniq]
+
+not_used_in_php = []
+keys.split("\n").each do |key|
+  %x[git grep "#{key}" -- #{DIR}/data/web/*.php #{DIR}/data/web/inc #{DIR}/data/web/modals]
+  if $?.exitstatus > 0
+    not_used_in_php << key
+  end
+end
+
+# \['user'\]\['username'\]
+# \['user'\]\['waiting'\]
+# \['warning'\]\['spam_alias_temp_error'\]
+
+not_used = []
+not_used_in_php.each do |string|
+  section = string.scan(/([a-z]+)/)[0][0]
+  key     = string.scan(/([a-z]+)/)[1][0]
+  %x[git grep lang.#{key} -- #{DIR}/data/web/js/#{section}.js #{DIR}/data/web/js/debug.js]
+  if $?.exitstatus > 0
+    not_used << string
+  end
+end
+
+puts "# Remove unused translation keys:"
+not_used.each do |key|
+  puts "sed -i \"/\\$lang#{key}.*;/d\" data/web/lang/lang.??.php"
+end
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml
new file mode 100644
index 0000000..a080fb3
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml
@@ -0,0 +1,45 @@
+version: '2.1'
+services:
+  unbound-mailcow:
+    build: ./data/Dockerfiles/unbound
+
+  clamd-mailcow:
+    build: ./data/Dockerfiles/clamd
+
+  rspamd-mailcow:
+    build: ./data/Dockerfiles/rspamd
+
+  php-fpm-mailcow:
+    build: ./data/Dockerfiles/phpfpm
+
+  sogo-mailcow:
+    build:
+      context: ./data/Dockerfiles/sogo
+      dockerfile: Dockerfile
+      args:
+        - SOGO_DEBIAN_REPOSITORY=http://packages.inverse.ca/SOGo/nightly/5/debian/
+
+  dovecot-mailcow:
+    build: ./data/Dockerfiles/dovecot
+
+  postfix-mailcow:
+    build: ./data/Dockerfiles/postfix
+
+  acme-mailcow:
+    build: ./data/Dockerfiles/acme
+
+  netfilter-mailcow:
+    build: ./data/Dockerfiles/netfilter
+
+  watchdog-mailcow:
+    build: ./data/Dockerfiles/watchdog
+
+  dockerapi-mailcow:
+    build: ./data/Dockerfiles/dockerapi
+
+  solr-mailcow:
+    build: ./data/Dockerfiles/solr
+
+  olefy-mailcow:
+    build: ./data/Dockerfiles/olefy
+
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/EXTERNAL_DNS/docker-compose.override.yml b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/EXTERNAL_DNS/docker-compose.override.yml
new file mode 100644
index 0000000..74763bf
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/EXTERNAL_DNS/docker-compose.override.yml
@@ -0,0 +1,44 @@
+version: '2.1'
+services:
+
+    clamd-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    rspamd-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    php-fpm-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    sogo-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    dovecot-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    postfix-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    nginx-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    acme-mailcow:
+      dns:
+        - my.resolvers.ip.addr
+
+    watchdog-mailcow:
+      environment:
+        - CHECK_UNBOUND=0
+      dns:
+        - my.resolvers.ip.addr
+
+    dockerapi-mailcow:
+      dns:
+        - my.resolvers.ip.addr
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml
new file mode 100644
index 0000000..7d4424e
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml
@@ -0,0 +1,31 @@
+version: '2.1'
+services:
+
+    php-fpm-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    sogo-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    dovecot-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    postfix-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    acme-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    watchdog-mailcow:
+      volumes:
+        - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
+
+    mysql-mailcow:
+      image: alpine:3.10
+      command: /bin/true
+      restart: "no"
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml
new file mode 100644
index 0000000..21d391b
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml
@@ -0,0 +1,20 @@
+##
+## Set haproxy_trusted_networks in Dovecots extra.conf!
+#ä
+
+version: '2.1'
+services:
+
+    dovecot-mailcow:
+      ports:
+        - "${IMAP_PORT_HAPROXY:-127.0.0.1:10143}:10143"
+        - "${IMAPS_PORT_HAPROXY:-127.0.0.1:10993}:10993"
+        - "${POP_PORT_HAPROXY:-127.0.0.1:10110}:10110"
+        - "${POPS_PORT_HAPROXY:-127.0.0.1:10995}:10995"
+        - "${SIEVE_PORT_HAPROXY:-127.0.0.1:14190}:14190"
+
+    postfix-mailcow:
+      ports:
+        - "${SMTP_PORT_HAPROXY:-127.0.0.1:10025}:10025"
+        - "${SMTPS_PORT_HAPROXY:-127.0.0.1:10465}:10465"
+        - "${SUBMISSION_PORT_HAPROXY:-127.0.0.1:10587}:10587"
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/expiry-dates.sh b/mailcow/src/mailcow-dockerized/helper-scripts/expiry-dates.sh
new file mode 100644
index 0000000..1554b70
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/expiry-dates.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+[[ -f mailcow.conf ]] && source mailcow.conf
+[[ -f ../mailcow.conf ]] && source ../mailcow.conf
+
+POSTFIX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:25 -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2)
+DOVECOT=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2)
+NGINX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:443 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2)
+echo TLS expiry dates:
+echo Postfix: ${POSTFIX}
+echo Dovecot: ${DOVECOT}
+echo Nginx: ${NGINX}
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/mailcow-reset-admin.sh b/mailcow/src/mailcow-dockerized/helper-scripts/mailcow-reset-admin.sh
new file mode 100755
index 0000000..4afd14c
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/mailcow-reset-admin.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+[[ -f mailcow.conf ]] && source mailcow.conf
+[[ -f ../mailcow.conf ]] && source ../mailcow.conf
+
+if [[ -z ${DBUSER} ]] || [[ -z ${DBPASS} ]] || [[ -z ${DBNAME} ]]; then
+	echo "Cannot find mailcow.conf, make sure this script is run from within the mailcow folder."
+	exit 1
+fi
+
+echo -n "Checking MySQL service... "
+if [[ -z $(docker ps -qf name=mysql-mailcow) ]]; then
+	echo "failed"
+	echo "MySQL (mysql-mailcow) is not up and running, exiting..."
+	exit 1
+fi
+
+echo "OK"
+read -r -p "Are you sure you want to reset the mailcow administrator account? [y/N] " response
+response=${response,,}    # tolower
+if [[ "$response" =~ ^(yes|y)$ ]]; then
+	echo -e "\nWorking, please wait..."
+	docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM admin WHERE username='admin';"
+  docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM domain_admins WHERE username='admin';"
+	docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "INSERT INTO admin (username, password, superadmin, active) VALUES ('admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, 1);"
+	docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM tfa WHERE username='admin';"
+	echo "
+Reset credentials:
+---
+Username: admin
+Password: moohoo
+TFA: none
+"
+else
+	echo "Operation canceled."
+fi
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/nextcloud.sh b/mailcow/src/mailcow-dockerized/helper-scripts/nextcloud.sh
new file mode 100755
index 0000000..41434b7
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/nextcloud.sh
@@ -0,0 +1,166 @@
+#!/usr/bin/env bash
+
+for bin in curl dirmngr; do
+  if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
+done
+
+[[ -z ${1} ]] && NC_HELP=y
+
+while [ "$1" != '' ]; do
+  case "${1}" in
+    -p|--purge) NC_PURGE=y && shift;;
+    -i|--install) NC_INSTALL=y && shift;;
+    -r|--resetpw) NC_RESETPW=y && shift;;
+    -h|--help) NC_HELP=y && shift;;
+    *) echo "Unknown parameter: ${1}" && shift;;
+  esac
+done
+
+if [[ ${NC_HELP} == "y" ]]; then
+  printf 'Usage:\n\n'
+  printf '  -p|--purge\n    Purge Nextcloud\n'
+  printf '  -i|--install\n    Install Nextcloud\n'
+  printf '  -r|--resetpw\n    Reset password\n\n'
+  exit 0
+fi
+
+[[ ${NC_PURGE} == "y" ]] && [[ ${NC_INSTALL} == "y" ]] && { echo "Cannot use -p and -i at the same time!"; exit 1; }
+[[ ${NC_PURGE} == "y" ]] && [[ ${NC_RESETPW} == "y" ]] && { echo "Cannot use -p and -r at the same time!"; exit 1; }
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd ${SCRIPT_DIR}/../
+source mailcow.conf
+
+if [[ ${NC_PURGE} == "y" ]]; then
+  read -r -p "Are you sure you want to purge Nextcloud? [y/N] " response
+  response=${response,,}
+  if [[ ! "$response" =~ ^(yes|y)$ ]]; then
+    echo "OK, aborting."
+    exit 1
+  fi
+
+  docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \
+    "$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'nc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)"
+  docker exec -it $(docker ps -f name=redis-mailcow -q) /bin/sh -c ' cat <<EOF | redis-cli
+SELECT 10
+FLUSHDB
+EOF
+'
+  if [ -d ./data/web/nextcloud/config ]; then
+    mv ./data/web/nextcloud/config/ ./data/conf/nextcloud-config-folder-$(date +%s).bak
+  fi
+  [[ -d ./data/web/nextcloud ]] && rm -rf ./data/web/nextcloud
+
+  [[ -f ./data/conf/nginx/site.nextcloud.custom ]] && mv ./data/conf/nginx/site.nextcloud.custom ./data/conf/nginx/site.nextcloud.custom-$(date +%s).bak
+  [[ -f ./data/conf/nginx/nextcloud.conf ]] && mv ./data/conf/nginx/nextcloud.conf ./data/conf/nginx/nextcloud.conf-$(date +%s).bak
+
+  docker restart $(docker ps -aqf name=nginx-mailcow)
+
+elif [[ ${NC_UPDATE} == "y" ]]; then
+  exit;
+  read -r -p "Are you sure you want to update Nextcloud? [y/N] " response
+  response=${response,,}
+  if [[ ! "$response" =~ ^(yes|y)$ ]]; then
+    echo "OK, aborting."
+    exit 1
+  fi
+
+  if [ ! -f data/web/nextcloud/occ ]; then
+    echo "Nextcloud occ not found. Is Nextcloud installed?"
+    exit 1
+  fi
+  if ! grep -q 'installed: true' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then
+    echo "Nextcloud seems not to be installed."
+    exit 1
+  elif ! grep -q 'version: 19\.' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then
+    echo "Cannot upgrade to new major version, please update manually."
+    exit 1
+  else
+    curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-19.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
+      && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
+      && rm nextcloud.tar.bz2 \
+      && mkdir -p ./data/web/nextcloud/data \
+      && chmod +x ./data/web/nextcloud/occ \
+       docker exec -it $(docker ps -f name=php-fpm-mailcow -q) bash -c "chown www-data:www-data -R /web/nextcloud" \
+       docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings upgrade"
+  fi
+
+elif [[ ${NC_INSTALL} == "y" ]]; then
+  NC_SUBD=
+  while [[ -z ${NC_SUBD} ]]; do
+    read -p "Subdomain to run Nextcloud from [format: nextcloud.domain.tld]: " NC_SUBD
+  done
+  if ! ping -q -c2 ${NC_SUBD} > /dev/null 2>&1 ; then
+    read -p "Cannot ping subdomain, continue anyway? [y|N] " NC_CONT_FAIL
+    [[ ! ${NC_CONT_FAIL,,} =~ ^(yes|y)$ ]] && { echo "Ok, exiting..."; exit 1; }
+  fi
+
+  ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
+
+  curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-19.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
+    && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
+    && rm nextcloud.tar.bz2 \
+    && mkdir -p ./data/web/nextcloud/data \
+    && chmod +x ./data/web/nextcloud/occ
+
+  docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud"
+  docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings maintenance:install \
+    --database mysql \
+    --database-host mysql \
+    --database-name ${DBNAME} \
+    --database-user ${DBUSER} \
+    --database-pass ${DBPASS} \
+    --database-table-prefix nc_ \
+    --admin-user admin \
+    --admin-pass ${ADMIN_NC_PASS} \
+      --data-dir /web/nextcloud/data
+
+  docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings config:system:set redis host --value=redis --type=string; \
+    /web/nextcloud/occ --no-warnings config:system:set redis port --value=6379 --type=integer; \
+    /web/nextcloud/occ --no-warnings config:system:set redis timeout --value=0.0 --type=integer; \
+    /web/nextcloud/occ --no-warnings config:system:set redis dbindex --value=10 --type=integer; \
+    /web/nextcloud/occ --no-warnings config:system:set memcache.locking --value='\OC\Memcache\Redis' --type=string; \
+    /web/nextcloud/occ --no-warnings config:system:set memcache.local --value='\OC\Memcache\Redis' --type=string; \
+    /web/nextcloud/occ --no-warnings config:system:set trusted_domains 1 --value=${NC_SUBD}; \
+    /web/nextcloud/occ --no-warnings config:system:set trusted_proxies 0 --value=${IPV6_NETWORK}; \
+    /web/nextcloud/occ --no-warnings config:system:set trusted_proxies 1 --value=${IPV4_NETWORK}.0/24; \
+    /web/nextcloud/occ --no-warnings config:system:set overwritehost --value=${NC_SUBD}; \
+    /web/nextcloud/occ --no-warnings config:system:set overwriteprotocol --value=https; \
+    /web/nextcloud/occ --no-warnings config:system:set overwritewebroot --value=/; \
+    /web/nextcloud/occ --no-warnings config:system:set mail_smtpmode --value=smtp; \
+    /web/nextcloud/occ --no-warnings config:system:set mail_smtpauthtype --value=LOGIN; \
+    /web/nextcloud/occ --no-warnings config:system:set mail_from_address --value=nextcloud; \
+    /web/nextcloud/occ --no-warnings config:system:set mail_domain --value=${MAILCOW_HOSTNAME}; \
+    /web/nextcloud/occ --no-warnings config:system:set mail_smtphost --value=postfix; \
+    /web/nextcloud/occ --no-warnings config:system:set mail_smtpport --value=588; \
+    /web/nextcloud/occ --no-warnings db:convert-filecache-bigint -n"
+
+    # Not installing by default, broke too often
+    #/web/nextcloud/occ --no-warnings app:install user_external; \
+    #/web/nextcloud/occ --no-warnings config:system:set user_backends 0 arguments 0 --value={dovecot:143/imap/tls/novalidate-cert}; \
+    #/web/nextcloud/occ --no-warnings config:system:set user_backends 0 class --value=OC_User_IMAP; \
+
+    cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/
+    sed -i "s/NC_SUBD/${NC_SUBD}/g" ./data/conf/nginx/nextcloud.conf
+
+  echo "Restarting Nginx..."
+  docker restart $(docker ps -aqf name=nginx-mailcow)
+
+  echo "Login as admin with password: ${ADMIN_NC_PASS}"
+
+elif [[ ${NC_RESETPW} == "y" ]]; then
+    printf 'You are about to set a new password for a Nextcloud user.\n\nDo not use this option if your Nextcloud is configured to use mailcow for authentication.\nSet a new password for the corresponding mailbox in mailcow, instead.\n\n'
+    read -r -p "Continue? [y/N] " response
+    response=${response,,}
+    if [[ ! "$response" =~ ^(yes|y)$ ]]; then
+      echo "OK, aborting."
+      exit 1
+    fi
+
+    NC_USER=
+    while [[ -z ${NC_USER} ]]; do
+      read -p "Enter the username: " NC_USER
+    done
+    docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ user:resetpassword ${NC_USER}
+
+fi
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/reset-learns.sh b/mailcow/src/mailcow-dockerized/helper-scripts/reset-learns.sh
new file mode 100755
index 0000000..9fd1123
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/reset-learns.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+read -r -p "Are you sure you want to reset learned hashes from Rspamd (fuzzy, bayes, neural)? [y/N] " response
+response=${response,,}    # tolower
+if [[ "$response" =~ ^(yes|y)$ ]]; then
+  echo "Working, please wait..."
+  REDIS_ID=$(docker ps -qf name=redis-mailcow)
+  RSPAMD_ID=$(docker ps -qf name=rspamd-mailcow)
+
+  if [ -z ${REDIS_ID} ] || [ -z ${RSPAMD_ID} ]; then
+    echo "Cannot determine Redis or Rspamd container ID"
+    exit 1
+  else
+    echo "Stopping Rspamd container"
+    docker stop ${RSPAMD_ID}
+    echo "LUA will return nil when it succeeds or print a warning/error when it fails."
+    echo "Deleting all RS* keys - if any"
+    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'RS*'
+    echo "Deleting all BAYES* keys - if any"
+    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'BAYES*'
+    echo "Deleting all learned* keys - if any"
+    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'learned*'
+    echo "Deleting all fuzzy* keys - if any"
+    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'fuzzy*'
+    echo "Deleting all tRFANN* keys - if any"
+    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'tRFANN*'
+    echo "Starting Rspamd container"
+    docker start ${RSPAMD_ID}
+  fi
+fi
diff --git a/mailcow/src/mailcow-dockerized/helper-scripts/update_postscreen_whitelist.sh b/mailcow/src/mailcow-dockerized/helper-scripts/update_postscreen_whitelist.sh
new file mode 100644
index 0000000..8dd1b83
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/helper-scripts/update_postscreen_whitelist.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+WORKING_DIR=${SCRIPT_DIR}/postwhite_tmp
+SPFTOOLS_DIR=${WORKING_DIR}/spf-tools
+POSTWHITE_DIR=${WORKING_DIR}/postwhite
+POSTWHITE_CONF=${POSTWHITE_DIR}/postwhite.conf
+
+COSTOM_HOSTS="web.de gmx.net mail.de freenet.de arcor.de unity-mail.de"
+STATIC_HOSTS=(
+    "194.25.134.0/24 permit # t-online.de"
+)
+
+mkdir ${SCRIPT_DIR}/postwhite_tmp
+git clone https://github.com/spf-tools/spf-tools.git ${SPFTOOLS_DIR}
+git clone https://github.com/stevejenkins/postwhite.git ${POSTWHITE_DIR}
+
+function set_config() {
+    sudo sed -i "s@^\($1\s*=\s*\).*\$@\1$2@" ${POSTWHITE_CONF}
+}
+
+set_config custom_hosts ${COSTOM_HOSTS}
+set_config reload_postfix no
+set_config postfixpath /.
+set_config spftoolspath ${WORKING_DIR}/spf-tools
+set_config whitelist .${SCRIPT_DIR}/../data/conf/postfix/postscreen_access.cidr
+set_config yahoo_static_hosts ${POSTWHITE_DIR}/yahoo_static_hosts.txt
+
+cd ${POSTWHITE_DIR}
+./postwhite ${POSTWHITE_CONF}
+
+( IFS=$'\n'; echo "${STATIC_HOSTS[*]}" >> "${SCRIPT_DIR}/../data/conf/postfix/postscreen_access.cidr")
+
+rm -r ${WORKING_DIR}