diff --git a/.gitignore b/.gitignore index 9de43f40..0d081fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,14 +7,18 @@ data/conf/nginx/listen*active data/conf/nginx/server_name.active data/conf/postfix/sql data/conf/dovecot/sql +data/conf/nextcloud-*.bak data/web/inc/vars.local.inc.php data/assets/ssl/* .vscode/* data/web/.well-known/acme-challenge +data/web/nextcloud/ data/conf/rspamd/local.d/* data/conf/rspamd/override.d/* !data/conf/nginx/dynmaps.conf !data/conf/nginx/site.conf data/conf/nginx/*.conf +data/conf/nginx/*.custom +data/conf/nginx/*.bak data/conf/dovecot/extra.conf data/conf/rspamd/custom/* diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index b3fd77fd..1554e6a8 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -9,8 +9,9 @@ RUN apk add --update --no-cache \ openssl \ bind-tools \ jq \ - mariadb-client + mariadb-client \ + tini COPY docker-entrypoint.sh /srv/docker-entrypoint.sh -ENTRYPOINT ["/srv/docker-entrypoint.sh"] +CMD ["/sbin/tini", "-g", "--", "/srv/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index 6715f791..6d3d7273 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -o pipefail +exec 5>&1 ACME_BASE=/var/lib/acme SSL_EXAMPLE=/var/lib/ssl-example @@ -6,24 +8,40 @@ SSL_EXAMPLE=/var/lib/ssl-example mkdir -p ${ACME_BASE}/acme/private restart_containers(){ - for container in $*; do - echo "Restarting ${container}..." - curl -X POST \ - --unix-socket /var/run/docker.sock \ - "http/containers/${container}/restart" - done + for container in $*; do + echo "Restarting ${container}..." + curl -X POST http://dockerapi:8080/containers/${container}/restart + done +} + +log_f() { + if [[ ${2} == "no_nl" ]]; then + echo -n "$(date) - ${1}" + elif [[ ${2} == "no_date" ]]; then + echo "${1}" + else + echo "$(date) - ${1}" + fi +} + +array_diff() { + # https://stackoverflow.com/questions/2312762, Alex Offshore + eval local ARR1=\(\"\${$2[@]}\"\) + eval local ARR2=\(\"\${$3[@]}\"\) + local IFS=$'\n' + mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort)) } verify_hash_match(){ - CERT_HASH=$(openssl x509 -noout -modulus -in "${1}" | openssl md5) - KEY_HASH=$(openssl rsa -noout -modulus -in "${2}" | openssl md5) - if [[ ${CERT_HASH} != ${KEY_HASH} ]]; then - echo "Certificate and key hashes do not match!" - return 1 - else - echo "Verified hashes." - return 0 - fi + CERT_HASH=$(openssl x509 -noout -modulus -in "${1}" | openssl md5) + KEY_HASH=$(openssl rsa -noout -modulus -in "${2}" | openssl md5) + if [[ ${CERT_HASH} != ${KEY_HASH} ]]; then + log_f "Certificate and key hashes do not match!" + return 1 + else + log_f "Verified hashes." + return 0 + fi } get_ipv4(){ @@ -31,7 +49,7 @@ get_ipv4(){ local IPV4_SRCS= local TRY= IPV4_SRCS[0]="api.ipify.org" - IPV4_SRCS[1]="ifconfig.co" + IPV4_SRCS[1]="ifconfig.co"- IPV4_SRCS[2]="icanhazip.com" IPV4_SRCS[3]="v4.ident.me" IPV4_SRCS[4]="ipecho.net/plain" @@ -47,224 +65,243 @@ get_ipv4(){ [[ ! -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 - ISSUER=$(openssl x509 -in ${ACME_BASE}/cert.pem -noout -issuer) - if [[ ${ISSUER} != *"Let's Encrypt"* && ${ISSUER} != *"mailcow"* ]]; then - echo "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..." - sleep 3650d - exec $(readlink -f "$0") - else - declare -a SAN_ARRAY_NOW - SAN_NAMES=$(openssl x509 -noout -text -in ${ACME_BASE}/cert.pem | awk '/X509v3 Subject Alternative Name/ {getline;gsub(/ /, "", $0); print}' | tr -d "DNS:") - if [[ ! -z ${SAN_NAMES} ]]; then - IFS=',' read -a SAN_ARRAY_NOW <<< ${SAN_NAMES} - echo "Found Let's Encrypt or mailcow snake-oil CA issued certificate with SANs: ${SAN_ARRAY_NOW[*]}" - fi - fi + ISSUER=$(openssl x509 -in ${ACME_BASE}/cert.pem -noout -issuer) + if [[ ${ISSUER} != *"Let's Encrypt"* && ${ISSUER} != *"mailcow"* ]]; then + log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..." + sleep 3650d + exec $(readlink -f "$0") + else + declare -a SAN_ARRAY_NOW + SAN_NAMES=$(openssl x509 -noout -text -in ${ACME_BASE}/cert.pem | awk '/X509v3 Subject Alternative Name/ {getline;gsub(/ /, "", $0); print}' | tr -d "DNS:") + if [[ ! -z ${SAN_NAMES} ]]; then + IFS=',' read -a SAN_ARRAY_NOW <<< ${SAN_NAMES} + log_f "Found Let's Encrypt or mailcow snake-oil CA issued certificate with SANs: ${SAN_ARRAY_NOW[*]}" + fi + fi else - if [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then - if verify_hash_match ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/privkey.pem; then - echo "Restoring previous acme certificate and restarting script..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - exec env TRIGGER_RESTART=1 $(readlink -f "$0") - fi - ISSUER="mailcow" - else - echo "Restoring mailcow snake-oil certificates and restarting script..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - exec env TRIGGER_RESTART=1 $(readlink -f "$0") - fi + if [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then + if verify_hash_match ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/privkey.pem; then + log_f "Restoring previous acme certificate and restarting script..." + cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + # Restarting with env var set to trigger a restart, + exec env TRIGGER_RESTART=1 $(readlink -f "$0") + fi + ISSUER="mailcow" + else + log_f "Restoring mailcow snake-oil certificates and restarting script..." + cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem + cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem + exec env TRIGGER_RESTART=1 $(readlink -f "$0") + fi fi +while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for database to come up..." + sleep 2 +done + while true; do - if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..." - sleep 3650d - exec $(readlink -f "$0") - fi - if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - SKIP_IP_CHECK=y - fi - unset SQL_DOMAIN_ARR - unset VALIDATED_CONFIG_DOMAINS - unset ADDITIONAL_VALIDATED_SAN - declare -a SQL_DOMAIN_ARR - declare -a VALIDATED_CONFIG_DOMAINS - declare -a ADDITIONAL_VALIDATED_SAN - IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}" - IPV4=$(get_ipv4) - # Container ids may have changed - CONTAINERS_RESTART=($(curl --silent --unix-socket /var/run/docker.sock http/containers/json | jq -rc 'map(select(.Names[] | contains ("nginx-mailcow") or contains ("postfix-mailcow") or contains ("dovecot-mailcow"))) | .[] .Id' | tr "\n" " ")) + if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..." + sleep 365d + exec $(readlink -f "$0") + fi + if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + SKIP_IP_CHECK=y + fi + unset SQL_DOMAIN_ARR + unset VALIDATED_CONFIG_DOMAINS + unset ADDITIONAL_VALIDATED_SAN + declare -a SQL_DOMAIN_ARR + declare -a VALIDATED_CONFIG_DOMAINS + declare -a ADDITIONAL_VALIDATED_SAN + IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}" + IPV4=$(get_ipv4) + # Container ids may have changed + CONTAINERS_RESTART=($(curl --silent http://dockerapi:8080/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("nginx-mailcow") or contains("postfix-mailcow") or contains("dovecot-mailcow")) | .id' | tr "\n" " ")) - while read domain; do - SQL_DOMAIN_ARR+=("${domain}") - done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs) - while read alias_domain; do - SQL_DOMAIN_ARR+=("${alias_domain}") - done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) + log_f "Waiting for domain tables... " no_nl + while [[ -z ${DOMAIN_TABLE} ]]; do + DOMAIN_TABLE=$(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) + [[ -z ${DOMAIN_TABLE} ]] && sleep 10 + done + log_f "OK" no_date - for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do - A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1) - if [[ ! -z ${A_CONFIG} ]]; then - echo "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}" - if [[ ${IPV4:-ERR} == ${A_CONFIG} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - echo "Confirmed A record autoconfig.${SQL_DOMAIN}" - VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}") - else - echo "Cannot match your IP ${IPV4} against hostname autoconfig.${SQL_DOMAIN} (${A_CONFIG})" - fi - else - echo "No A record for autoconfig.${SQL_DOMAIN} found" - fi + while read domains; do + SQL_DOMAIN_ARR+=("${domains}") + done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 UNION SELECT alias_domain FROM alias_domain" -Bs) + + for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do + A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1) + if [[ ! -z ${A_CONFIG} ]]; then + log_f "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}" + if [[ ${IPV4:-ERR} == ${A_CONFIG} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then + log_f "Confirmed A record autoconfig.${SQL_DOMAIN}" + VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}") + else + log_f "Cannot match your IP ${IPV4} against hostname autoconfig.${SQL_DOMAIN} (${A_CONFIG})" + fi + else + log_f "No A record for autoconfig.${SQL_DOMAIN} found" + fi A_DISCOVER=$(dig A autodiscover.${SQL_DOMAIN} +short | tail -n 1) - if [[ ! -z ${A_DISCOVER} ]]; then - echo "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}" - if [[ ${IPV4:-ERR} == ${A_DISCOVER} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - echo "Confirmed A record autodiscover.${SQL_DOMAIN}" - VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}") - else - echo "Cannot match your IP ${IPV4} against hostname autodiscover.${SQL_DOMAIN} (${A_DISCOVER})" - fi - else - echo "No A record for autodiscover.${SQL_DOMAIN} found" - fi - done + if [[ ! -z ${A_DISCOVER} ]]; then + log_f "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}" + if [[ ${IPV4:-ERR} == ${A_DISCOVER} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then + log_f "Confirmed A record autodiscover.${SQL_DOMAIN}" + VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}") + else + log_f "Cannot match your IP ${IPV4} against hostname autodiscover.${SQL_DOMAIN} (${A_DISCOVER})" + fi + else + log_f "No A record for autodiscover.${SQL_DOMAIN} found" + fi + done - A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1) - if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then - echo "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}" - if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - echo "Confirmed A record ${MAILCOW_HOSTNAME}" - VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - else - echo "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME}) " - fi - else - echo "No A record for ${MAILCOW_HOSTNAME} found" - fi + A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1) + if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then + log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}" + if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then + log_f "Confirmed A record ${MAILCOW_HOSTNAME}" + VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + else + log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME}) " + fi + else + log_f "No A record for ${MAILCOW_HOSTNAME} found" + fi - for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do - if [[ ${SAN} == ${MAILCOW_HOSTNAME} ]]; then - continue - fi - A_SAN=$(dig A ${SAN} +short | tail -n 1) - if [[ ! -z ${A_SAN} ]]; then - echo "Found A record for ${SAN}: ${A_SAN}" - if [[ ${IPV4:-ERR} == ${A_SAN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then - echo "Confirmed A record ${SAN}" - ADDITIONAL_VALIDATED_SAN+=("${SAN}") - else - echo "Cannot match your IP against hostname ${SAN}" - fi - else - echo "No A record for ${SAN} found" - fi - done + for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do + if [[ ${SAN} == ${MAILCOW_HOSTNAME} ]]; then + continue + fi + A_SAN=$(dig A ${SAN} +short | tail -n 1) + if [[ ! -z ${A_SAN} ]]; then + log_f "Found A record for ${SAN}: ${A_SAN}" + if [[ ${IPV4:-ERR} == ${A_SAN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then + log_f "Confirmed A record ${SAN}" + ADDITIONAL_VALIDATED_SAN+=("${SAN}") + else + log_f "Cannot match your IP against hostname ${SAN}" + fi + else + log_f "No A record for ${SAN} found" + fi + done # Unique elements - ALL_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) - if [[ -z ${ALL_VALIDATED[*]} ]]; then - echo "Cannot validate hostnames, skipping Let's Encrypt for 1 hour." - echo "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently." - sleep 1h - exec $(readlink -f "$0") - fi + ALL_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + if [[ -z ${ALL_VALIDATED[*]} ]]; then + 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." + sleep 1h + exec $(readlink -f "$0") + fi - ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${ALL_VALIDATED[*]} | tr ' ' '\n' | sort | uniq -u )) - if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then - DATE=$(date +%Y-%m-%d_%H_%M_%S) - echo "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..." - mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/ - [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/ - [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ - [[ -f ${ACME_BASE}/acme/cert.pem ]] && mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/private/${DATE}.bak/ # Keep key for TLSA 3 1 1 records - fi + array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED + if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then + DATE=$(date +%Y-%m-%d_%H_%M_%S) + log_f "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..." + mkdir -p ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/ + [[ -f ${ACME_BASE}/acme/cert.pem ]] && mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/private/${DATE}.bak/ + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/private/${DATE}.bak/ # Keep key for TLSA 3 1 1 records + fi - acme-client \ - -v -e -b -N -n \ - -f ${ACME_BASE}/acme/private/account.key \ - -k ${ACME_BASE}/acme/private/privkey.pem \ - -c ${ACME_BASE}/acme \ - ${ALL_VALIDATED[*]} + ACME_RESPONSE=$(acme-client \ + -v -e -b -N -n \ + -f ${ACME_BASE}/acme/private/account.key \ + -k ${ACME_BASE}/acme/private/privkey.pem \ + -c ${ACME_BASE}/acme \ + ${ALL_VALIDATED[*]} 2>&1 | tee /dev/fd/5) - case "$?" in - 0) # new certs - # cp the new certificates and keys - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + case "$?" in + 0) # new certs + # cp the new certificates and keys + cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - # restart docker containers - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - echo "Certificate was successfully requested, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - fi - restart_containers ${CONTAINERS_RESTART[*]} - ;; - 1) # failure - if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then - echo "Error requesting certificate, restoring previous certificate from backup and restarting containers...." - cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 + # restart docker containers + if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then + log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..." + cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem + cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem + fi + restart_containers ${CONTAINERS_RESTART[*]} + ;; + 1) # failure + if [[ $ACME_RESPONSE =~ "No registration exists" ]]; then + log_f "Registration keys are invalid, deleting old keys and restarting..." + rm ${ACME_BASE}/acme/private/account.key + rm ${ACME_BASE}/acme/private/privkey.pem + exec $(readlink -f "$0") + fi + if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then + log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...." + cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then + log_f "Error requesting certificate, restoring from previous acme request and restarting containers..." + cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + fi + if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then + log_f "Error verifying certificates, restoring mailcow snake-oil and restarting containers..." + cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem + cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + fi + [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]} + log_f "Retrying in 30 minutes..." + sleep 30m + exec $(readlink -f "$0") + ;; + 2) # no change + if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then + log_f "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..." + cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + fi + if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then + log_f "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..." + cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + fi + [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]} + ;; + *) # unspecified + if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then + log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...." + cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then - echo "Error requesting certificate, restoring from previous acme request and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - echo "Error verifying certificates, restoring mailcow snake-oil and restarting containers..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]} - echo "Retrying in 30 minutes..." - sleep 30m - exec $(readlink -f "$0") - ;; - 2) # no change - if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then - echo "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - restart_containers ${CONTAINERS_RESTART[*]} - fi - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - echo "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - restart_containers ${CONTAINERS_RESTART[*]} - fi - ;; - *) # unspecified - if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then - echo "Error requesting certificate, restoring previous certificate from backup and restarting containers...." - cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then - echo "Error requesting certificate, restoring from previous acme request and restarting containers..." - cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem - cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then - echo "Error verifying certificates, restoring mailcow snake-oil..." - cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem - cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem - TRIGGER_RESTART=1 - fi - [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]} - sleep 3650d - ;; - esac + log_f "Error requesting certificate, restoring from previous acme request and restarting containers..." + cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + fi + if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then + log_f "Error verifying certificates, restoring mailcow snake-oil..." + cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem + cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem + TRIGGER_RESTART=1 + fi + [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]} + log_f "Retrying in 30 minutes..." + sleep 30m + exec $(readlink -f "$0") + ;; + esac - echo "ACME certificate validation done. Sleeping for another day." - sleep 1d + log_f "ACME certificate validation done. Sleeping for another day." + sleep 1d done diff --git a/data/Dockerfiles/acme/tini b/data/Dockerfiles/acme/tini new file mode 100755 index 00000000..6556c966 Binary files /dev/null and b/data/Dockerfiles/acme/tini differ diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index aa50b807..ec56bf1d 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -7,7 +7,7 @@ COPY dl_files.sh bootstrap.sh ./ # Installation RUN apk add --update \ - && apk add --no-cache clamav clamav-libunrar curl bash \ + && apk add --no-cache clamav clamav-libunrar curl bash tini \ && chmod +x /dl_files.sh \ && set -ex; /bin/bash /dl_files.sh \ && mkdir /run/clamav \ @@ -15,12 +15,14 @@ RUN apk add --update \ && chmod 750 /run/clamav \ && sed -i '/Foreground yes/s/^#//g' /etc/clamav/clamd.conf \ && sed -i '/TCPSocket 3310/s/^#//g' /etc/clamav/clamd.conf \ + && sed -i 's#LogFile /var/log/clamav/clamd.log#LogFile /tmp/logpipe_clamd#g' /etc/clamav/clamd.conf \ && sed -i 's/#PhishingSignatures yes/PhishingSignatures no/g' /etc/clamav/clamd.conf \ && sed -i 's/#PhishingScanURLs yes/PhishingScanURLs no/g' /etc/clamav/clamd.conf \ + && sed -i 's#UpdateLogFile /var/log/clamav/freshclam.log#UpdateLogFile /tmp/logpipe_freshclam#g' /etc/clamav/freshclam.conf \ && sed -i '/Foreground yes/s/^#//g' /etc/clamav/freshclam.conf # Port provision EXPOSE 3310 # AV daemon bootstrapping -CMD ["/bootstrap.sh"] +CMD ["/sbin/tini", "-g", "--", "/bootstrap.sh"] diff --git a/data/Dockerfiles/clamd/bootstrap.sh b/data/Dockerfiles/clamd/bootstrap.sh index 21b09788..ffe582c9 100755 --- a/data/Dockerfiles/clamd/bootstrap.sh +++ b/data/Dockerfiles/clamd/bootstrap.sh @@ -1,13 +1,34 @@ #!/bin/bash -touch /var/log/clamav/clamd.log /var/log/clamav/freshclam.log -chown -R clamav:clamav /var/log/clamav/ if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "SKIP_CLAMD=y, skipping ClamAV..." - exit 0 + echo "SKIP_CLAMD=y, skipping ClamAV..." + sleep 365d + exit 0 fi -freshclam -d & -clamd & +# Create log pipes +touch /var/log/clamav/clamd.log /var/log/clamav/freshclam.log +mkfifo -m 600 /tmp/logpipe_clamd +mkfifo -m 600 /tmp/logpipe_freshclam +chown -R clamav:clamav /var/log/clamav/ /tmp/logpipe_* +cat <> /tmp/logpipe_clamd 1>&2 & +cat <> /tmp/logpipe_freshclam 1>&2 & -tail -f /var/log/clamav/clamd.log /var/log/clamav/freshclam.log +# Prepare +BACKGROUND_TASKS=() + +freshclam -d & +BACKGROUND_TASKS+=($!) + +clamd & +BACKGROUND_TASKS+=($!) + +while true; do + for bg_task in ${BACKGROUND_TASKS[*]}; do + if ! kill -0 ${bg_task} 1>&2; then + echo "Worker ${bg_task} died, stopping container waiting for respawn..." + kill -TERM 1 + fi + sleep 10 + done +done diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile new file mode 100644 index 00000000..a5b3301d --- /dev/null +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -0,0 +1,8 @@ +FROM python:2-alpine +LABEL maintainer "Andre Peters " + +RUN apk add -U --no-cache iptables ip6tables +RUN pip install docker flask flask-restful + +COPY server.py / +CMD ["python2", "-u", "/server.py"] diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py new file mode 100644 index 00000000..a021ab54 --- /dev/null +++ b/data/Dockerfiles/dockerapi/server.py @@ -0,0 +1,62 @@ +from flask import Flask +from flask_restful import Resource, Api +from flask import jsonify +import docker + +docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock') +app = Flask(__name__) +api = Api(app) + +class containers_get(Resource): + def get(self): + containers = {} + for container in docker_client.containers.list(all=True): + containers.update({container.attrs['Id']: container.attrs}) + return containers + +class container_get(Resource): + def get(self, container_id): + if container_id and container_id.isalnum(): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + return container.attrs + else: + return jsonify(message='No or invalid id defined') + +class container_post(Resource): + def post(self, container_id, 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() + except: + return 'Error' + else: + return 'OK' + elif post_action == 'start': + try: + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.start() + except: + return 'Error' + else: + return 'OK' + elif post_action == 'restart': + try: + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.restart() + except: + return 'Error' + else: + return 'OK' + else: + return jsonify(message='Invalid action') + else: + return jsonify(message='Invalid container id or missing action') + +api.add_resource(containers_get, '/containers/json') +api.add_resource(container_get, '/containers//json') +api.add_resource(container_post, '/containers//') + +if __name__ == '__main__': + app.run(debug=False, host='0.0.0.0', port='8080') diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf index 292efc7d..216111f2 100644 --- a/data/Dockerfiles/dovecot/syslog-ng.conf +++ b/data/Dockerfiles/dovecot/syslog-ng.conf @@ -39,12 +39,13 @@ destination d_redis_cleanup { ); }; filter f_mail { facility(mail); }; +filter f_not_watchdog { not message("172\.22\.1\.248"); }; log { source(s_src); + filter(f_not_watchdog); destination(d_stdout); filter(f_mail); destination(d_redis_ui_log); destination(d_redis_f2b_channel); destination(d_redis_cleanup); - }; diff --git a/data/Dockerfiles/fail2ban/logwatch.py b/data/Dockerfiles/fail2ban/logwatch.py index 9615d53a..f1954489 100644 --- a/data/Dockerfiles/fail2ban/logwatch.py +++ b/data/Dockerfiles/fail2ban/logwatch.py @@ -14,7 +14,8 @@ import json yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$') if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)): - print "Skipping Fail2ban container..." + print "SKIP_FAIL2BAN=y, Skipping Fail2ban container..." + time.sleep(31536000) raise SystemExit r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index bd1837ec..fc6552fd 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,8 +1,17 @@ FROM php:7.1-fpm-alpine LABEL maintainer "Andre Peters " +ENV REDIS_PECL 3.1.4 +ENV MEMCACHED_PECL 3.0.3 +ENV APCU_PECL 5.1.8 +ENV IMAGICK_PECL 3.4.3 + RUN apk add -U --no-cache libxml2-dev \ icu-dev \ + imap-dev \ + libmemcached-dev \ + cyrus-sasl-dev \ + pcre-dev \ icu-libs \ redis \ mysql-client \ @@ -11,13 +20,42 @@ RUN apk add -U --no-cache libxml2-dev \ g++ \ make \ openssl \ - && pecl install redis \ + openssl-dev \ + samba-client \ + libpng \ + libpng-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + zlib-dev \ + libxpm-dev \ + c-client \ + imagemagick-dev \ + imagemagick \ + libtool \ + librsvg \ + && pear install channel://pear.php.net/Net_IDNA2-0.2.0 \ + channel://pear.php.net/Auth_SASL-1.1.0 \ + Net_IMAP \ + NET_SMTP \ + Mail_mime \ + && pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \ + && docker-php-ext-enable redis apcu memcached imagick \ && pecl clear-cache \ && docker-php-ext-configure intl \ - && docker-php-ext-install intl pdo pdo_mysql xmlrpc \ - && docker-php-ext-enable redis \ - && pear install channel://pear.php.net/Net_IDNA2-0.1.1 Auth_SASL Net_IMAP NET_SMTP Net_IDNA2 Mail_mime \ - && apk del autoconf g++ make libxml2-dev icu-dev + && docker-php-ext-install -j 4 intl pdo pdo_mysql xmlrpc gd zip pcntl opcache \ + && docker-php-ext-configure imap --with-imap --with-imap-ssl \ + && docker-php-ext-install -j 4 imap \ + && apk del --purge autoconf g++ make libxml2-dev icu-dev imap-dev openssl-dev cyrus-sasl-dev pcre-dev libpng-dev libpng-dev libjpeg-turbo-dev libwebp-dev zlib-dev imagemagick-dev \ + && { \ + echo 'opcache.enable=1'; \ + echo 'opcache.enable_cli=1'; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.revalidate_freq=1'; \ +} > /usr/local/etc/php/conf.d/opcache-recommended.ini + COPY ./docker-entrypoint.sh / diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 2456f3d9..326a6d18 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -4,8 +4,8 @@ trap "postfix stop" EXIT [[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/ if [[ -z $(grep null /etc/aliases) ]]; then - echo null: /dev/null >> /etc/aliases; - newaliases; + echo null: /dev/null >> /etc/aliases; + newaliases; fi cat < /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf @@ -13,7 +13,17 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT DISTINCT CASE WHEN '%d' IN (SELECT domain FROM domain WHERE relay_all_recipients=1 AND domain='%d' AND backupmx=1) THEN '%s' ELSE (SELECT goto FROM alias WHERE address='%s' AND active='1') END AS result; +query = SELECT DISTINCT + CASE WHEN '%d' IN ( + SELECT domain FROM domain + WHERE relay_all_recipients=1 + AND domain='%d' + AND backupmx=1 + ) + THEN '%s' ELSE ( + SELECT goto FROM alias WHERE address='%s' AND active='1' + ) + END AS result; EOF cat < /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf @@ -21,7 +31,16 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; +query = SELECT IF(EXISTS( + SELECT 'TLS_ACTIVE' FROM alias + LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto + WHERE (address='%s' + OR address IN ( + SELECT CONCAT('%u', '@', target_domain) FROM alias_domain + WHERE alias_domain='%d' + ) + ) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1' + ), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF cat < /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf @@ -31,9 +50,26 @@ hosts = mysql dbname = ${DBNAME} query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps FROM ( - SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address = '%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain = '%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'smtp:') AS 'transport' + SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias + LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto + WHERE (address = '%s' + OR address IN ( + SELECT CONCAT('%u', '@', target_domain) FROM alias_domain + WHERE alias_domain = '%d' + ) + ) + AND mailbox.tls_enforce_out = '1' + AND mailbox.active = '1' + ), 'smtp_enforced_tls:', 'smtp:') AS 'transport' UNION ALL - SELECT hostname AS transport FROM relayhosts LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id WHERE relayhosts.active = '1' AND domain = '%d' OR domain IN (SELECT target_domain FROM alias_domain WHERE alias_domain = '%d') + SELECT hostname AS transport FROM relayhosts + LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id + WHERE relayhosts.active = '1' + AND domain = '%d' + OR domain IN ( + SELECT target_domain FROM alias_domain + WHERE alias_domain = '%d' + ) ) AS transport_view; EOF @@ -43,7 +79,11 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts WHERE id IN (SELECT relayhost FROM domain WHERE CONCAT('@', domain) = '%s'); +query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts + WHERE id IN ( + SELECT relayhost FROM domain + WHERE CONCAT('@', domain) = '%s' + ); EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -51,7 +91,10 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql 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' +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 < /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf @@ -59,7 +102,11 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT username FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1' +query = SELECT username FROM mailbox, alias_domain + WHERE alias_domain.alias_domain = '%d' + AND mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) + AND mailbox.active = '1' + AND alias_domain.active='1' EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf @@ -67,7 +114,9 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT goto FROM alias WHERE address='%s' AND active='1'; +query = SELECT goto FROM alias + WHERE address='%s' + AND active='1'; EOF cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf @@ -75,7 +124,12 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1' UNION SELECT domain FROM domain WHERE domain='%s' AND active = '1' AND backupmx = '0' +query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1' + UNION + SELECT domain FROM domain + WHERE domain='%s' + AND active = '1' + AND backupmx = '0' EOF cat < /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf @@ -99,7 +153,39 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT goto FROM alias WHERE address='%s' AND active='1' AND (domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') OR domain in (SELECT target_domain FROM alias_domain WHERE alias_domain='%d' AND active='1')) UNION SELECT logged_in_as FROM sender_acl WHERE send_as='@%d' OR send_as='%s' OR send_as IN ( SELECT CONCAT ('@',target_domain) FROM alias_domain WHERE alias_domain = '%d') OR send_as IN ( SELECT CONCAT ('%u','@',target_domain) FROM alias_domain WHERE alias_domain = '%d' ) AND logged_in_as NOT IN (SELECT goto FROM alias WHERE address='%s') UNION SELECT username FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain) AND mailbox.active ='1' AND alias_domain.active='1' +# First select queries domain and alias_domain to determine if domains are active. +query = SELECT goto FROM alias + WHERE address='%s' + AND active='1' + AND (domain IN + (SELECT domain FROM domain + WHERE domain='%d' + AND active='1') + OR domain in ( + SELECT alias_domain FROM alias_domain + WHERE alias_domain='%d' + AND active='1' + ) + ) + UNION + SELECT logged_in_as FROM sender_acl + WHERE send_as='@%d' + OR send_as='%s' + OR send_as IN ( + SELECT CONCAT('@',target_domain) FROM alias_domain + WHERE alias_domain = '%d') + OR send_as IN ( + SELECT CONCAT('%u','@',target_domain) FROM alias_domain + WHERE alias_domain = '%d') + AND logged_in_as NOT IN ( + SELECT goto FROM alias + WHERE address='%s') + UNION + SELECT username FROM mailbox, alias_domain + WHERE alias_domain.alias_domain = '%d' + AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain) + AND mailbox.active ='1' + AND alias_domain.active='1' EOF cat < /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf @@ -107,7 +193,9 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() +query = SELECT goto FROM spamalias + WHERE address='%s' + AND validity >= UNIX_TIMESTAMP() EOF # Reset GPG key permissions @@ -124,9 +212,9 @@ postfix set-permissions postconf -c /opt/postfix/conf if [[ $? != 0 ]]; then - echo "Postfix configuration error, refusing to start." - exit 1 + echo "Postfix configuration error, refusing to start." + exit 1 else - postfix -c /opt/postfix/conf start - sleep 126144000 + postfix -c /opt/postfix/conf start + sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 55e76a95..b5ae6b54 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -1,5 +1,6 @@ [supervisord] nodaemon=true +user=root [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps @@ -12,14 +13,3 @@ autostart=true [program:postfix] command=/opt/postfix.sh autorestart=true - -[unix_http_server] -file=/var/tmp/supervisord.sock -chmod=0770 -chown=nobody:nogroup - -[supervisorctl] -serverurl=unix:///var/tmp/supervisord.sock - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index cfb76a16..9aa45e3a 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -39,8 +39,10 @@ destination d_redis_cleanup { ); }; filter f_mail { facility(mail); }; +filter f_skip_local { not facility (local0, local1, local2, local3, local4, local5, local6, local7); }; log { source(s_src); + filter(f_skip_local); destination(d_stdout); filter(f_mail); destination(d_redis_ui_log); diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 2913cefb..eb9238e1 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -22,7 +22,8 @@ COPY settings.conf /etc/rspamd/modules.d/settings.conf #COPY ratelimit.lua /usr/share/rspamd/lua/ratelimit.lua #COPY lua_util.lua /usr/share/rspamd/lib/lua_util.lua COPY docker-entrypoint.sh /docker-entrypoint.sh +COPY tini /sbin/tini ENTRYPOINT ["/docker-entrypoint.sh"] -CMD /usr/bin/rspamd -f -u _rspamd -g _rspamd +CMD ["/usr/bin/rspamd", "-f", "-u", "_rspamd", "-g", "_rspamd"] diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index c6903311..ae216570 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -2,4 +2,4 @@ chown -R _rspamd:_rspamd /var/lib/rspamd -exec "$@" +exec /sbin/tini -- "$@" diff --git a/data/Dockerfiles/rspamd/tini b/data/Dockerfiles/rspamd/tini new file mode 100755 index 00000000..6556c966 Binary files /dev/null and b/data/Dockerfiles/rspamd/tini differ diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index a9cefc47..1bfca949 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -28,8 +28,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN mkdir /usr/share/doc/sogo \ && touch /usr/share/doc/sogo/empty.sh \ - && apt-key adv --keyserver sks.labs.nic.cz --recv-key A04BE668 \ - && echo "deb http://www.axis.cz/linux/debian stretch sogo-v3" > /etc/apt/sources.list.d/sogo.list \ + && apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \ + && echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ stretch stretch" > /etc/apt/sources.list.d/sogo.list \ && apt-get update && apt-get install -y --force-yes \ sogo \ sogo-activesync \ diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index 4c611a39..2a889560 100644 --- a/data/Dockerfiles/sogo/supervisord.conf +++ b/data/Dockerfiles/sogo/supervisord.conf @@ -1,5 +1,6 @@ [supervisord] nodaemon=true +user=root [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps @@ -32,12 +33,3 @@ priority=3 startretries=10 autorestart=true stopwaitsecs=120 - -[inet_http_server] -port=9191 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface - -[supervisorctl] -serverurl=http://localhost:9191 diff --git a/data/Dockerfiles/watchdog/Dockerfile b/data/Dockerfiles/watchdog/Dockerfile new file mode 100644 index 00000000..9eb88335 --- /dev/null +++ b/data/Dockerfiles/watchdog/Dockerfile @@ -0,0 +1,33 @@ +FROM alpine:3.6 +LABEL maintainer "André Peters " + +# Installation +RUN apk add --update \ + && apk add --no-cache nagios-plugins-smtp \ + nagios-plugins-tcp \ + nagios-plugins-http \ + nagios-plugins-ping \ + curl \ + bash \ + jq \ + fcgi \ + nagios-plugins-mysql \ + nagios-plugins-dns \ + nagios-plugins-disk \ + bind-tools \ + redis \ + perl \ + perl-io-socket-ssl \ + perl-socket \ + perl-socket6 \ + perl-mime-lite \ + perl-term-readkey \ + tini \ + && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.8/smtp-cli -o /smtp-cli \ + && chmod +x smtp-cli + +COPY watchdog.sh /watchdog.sh + +ENTRYPOINT ["/sbin/tini", "-g", "--"] +# Less verbose +CMD /watchdog.sh 2> /dev/null diff --git a/data/Dockerfiles/watchdog/tini b/data/Dockerfiles/watchdog/tini new file mode 100755 index 00000000..6556c966 Binary files /dev/null and b/data/Dockerfiles/watchdog/tini differ diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh new file mode 100755 index 00000000..470025de --- /dev/null +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -0,0 +1,381 @@ +#!/bin/bash + +trap "exit" INT TERM +trap "kill 0" EXIT + +# Prepare +BACKGROUND_TASKS=() + +if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then + echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..." + sleep 365d + exec $(readlink -f "$0") +fi + +# Checks pipe their corresponding container name in this pipe +if [[ ! -p /tmp/com_pipe ]]; then + mkfifo /tmp/com_pipe +fi + +# Common functions +progress() { + SERVICE=${1} + TOTAL=${2} + CURRENT=${3} + DIFF=${4} + [[ -z ${DIFF} ]] && DIFF=0 + [[ -z ${TOTAL} || -z ${CURRENT} ]] && return + [[ ${CURRENT} -gt ${TOTAL} ]] && return + [[ ${CURRENT} -lt 0 ]] && CURRENT=0 + PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} )) + echo -ne "$(date) - ${SERVICE} health level: \e[7m${PERCENT}%\e[0m (${CURRENT}/${TOTAL}), health trend: " + [[ ${DIFF} =~ ^-[1-9] ]] && echo -en '[\e[41m \e[0m] ' || echo -en '[\e[42m \e[0m] ' + echo "(${DIFF})" +} + +log_to_redis() { + redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}")\"}" +} + +function mail_error() { + [[ -z ${1} ]] && return 1 + [[ -z ${2} ]] && return 2 + RCPT_DOMAIN=$(echo ${1} | awk -F @ {'print $NF'}) + RCPT_MX=$(dig +short ${RCPT_DOMAIN} mx | sort -n | awk '{print $2; exit}') + if [[ -z ${RCPT_MX} ]]; then + log_to_redis "Cannot determine MX for ${1}, skipping email notification..." + echo "Cannot determine MX for ${1}" + return 1 + fi + ./smtp-cli --missing-modules-ok \ + --subject="Watchdog: ${2} service hit the error rate limit" \ + --body-plain="Service was restarted, please check your mailcow installation." \ + --to=${1} \ + --from="watchdog@${MAILCOW_HOSTNAME}" \ + --server="${RCPT_MX}" \ + --hello-host=${MAILCOW_HOSTNAME} +} + + +get_container_ip() { + # ${1} is container + CONTAINER_ID= + CONTAINER_IP= + LOOP_C=1 + until [[ ${CONTAINER_IP} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || [[ ${LOOP_C} -gt 5 ]]; do + sleep 1 + CONTAINER_ID=$(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${1}\")) | .id") + if [[ ! -z ${CONTAINER_ID} ]]; then + CONTAINER_IP=$(curl --silent http://dockerapi:8080/containers/${CONTAINER_ID}/json | jq -r '.NetworkSettings.Networks[].IPAddress') + fi + LOOP_C=$((LOOP_C + 1)) + done + [[ ${LOOP_C} -gt 5 ]] && echo 240.0.0.0 || echo ${CONTAINER_IP} +} + +# Check functions +nginx_checks() { + err_count=0 + diff_c=0 + THRESHOLD=16 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip nginx-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_ping -4 -H ${host_ip} -w 2000,10% -c 4000,100% -p2 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u / -p 8081 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 "Nginx" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +mysql_checks() { + err_count=0 + diff_c=0 + THRESHOLD=12 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip mysql-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_mysql -H ${host_ip} -P 3306 -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql_query -H ${host_ip} -P 3306 -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +sogo_checks() { + err_count=0 + diff_c=0 + THRESHOLD=20 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip sogo-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /WebServerResources/css/theme-default.css -p 9192 -R md-default-theme 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\sGroupware" 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 "SOGo" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +postfix_checks() { + err_count=0 + diff_c=0 + THRESHOLD=16 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip postfix-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f watchdog -C "RCPT TO:null@localhost" -C DATA -C . -R 250 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -S 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Postfix" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +dovecot_checks() { + err_count=0 + diff_c=0 + THRESHOLD=24 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip dovecot-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 24 -f "watchdog" -C "RCPT TO:" -L -R "User doesn't exist" 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 993 -S -e "OK " 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 143 -e "OK " 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10001 -e "VERSION" 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 4190 -e "Dovecot ready" 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 "Dovecot" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +phpfpm_checks() { + err_count=0 + diff_c=0 + THRESHOLD=10 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip php-fpm-mailcow) + err_c_cur=${err_count} + cgi-fcgi -bind -connect ${host_ip}:9000 | grep "Content-type" 1>&2; err_count=$(( ${err_count} + ($? * 2))) + /usr/lib/nagios/plugins/check_ping -4 -H ${host_ip} -w 2000,10% -c 4000,100% -p2 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 "PHP-FPM" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +rspamd_checks() { + err_count=0 + diff_c=0 + THRESHOLD=10 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip rspamd-mailcow) + err_c_cur=${err_count} + SCORE=$(curl --silent ${host_ip}:11333/scan -d ' +To: null@localhost +From: watchdog@localhost + +Empty +' | jq -rc .required_score) + if [[ ${SCORE} != "9999" ]]; then + echo "Rspamd settings check failed" 1>&2 + err_count=$(( ${err_count} + 1)) + else + echo "Rspamd settings check succeeded" 1>&2 + fi + /usr/lib/nagios/plugins/check_ping -4 -H ${host_ip} -w 2000,10% -c 4000,100% -p2 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 "Rspamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +dns_checks() { + err_count=0 + diff_c=0 + THRESHOLD=28 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + host_ip=$(get_container_ip unbound-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_dns -H google.com 1>&2; err_count=$(( ${err_count} + ($? * 2))) + /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H google.com 1>&2; err_count=$(( ${err_count} + ($? * 2))) + dig +dnssec org. @${host_ip} | grep -E 'flags:.+ad' 1>&2; err_count=$(( ${err_count} + ($? * 2))) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Unbound" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + diff_c=0 + sleep $(( ( RANDOM % 30 ) + 10 )) + done + return 1 +} + +# Create watchdog agents +( +while true; do + if ! nginx_checks; then + log_to_redis "Nginx hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "nginx-mailcow" + echo -e "\e[31m$(date) - Nginx hit error limit\e[0m" + echo nginx-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! mysql_checks; then + log_to_redis "MySQL hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "mysql-mailcow" + echo -e "\e[31m$(date) - MySQL hit error limit\e[0m" + echo mysql-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! phpfpm_checks; then + log_to_redis "PHP-FPM hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "php-fpm-mailcow" + echo -e "\e[31m$(date) - PHP-FPM hit error limit\e[0m" + echo php-fpm-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! sogo_checks; then + log_to_redis "SOGo hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "sogo-mailcow" + echo -e "\e[31m$(date) - SOGo hit error limit\e[0m" + echo sogo-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! postfix_checks; then + log_to_redis "Postfix hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "postfix-mailcow" + echo -e "\e[31m$(date) - Postfix hit error limit\e[0m" + echo postfix-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! dovecot_checks; then + log_to_redis "Dovecot hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "dovecot-mailcow" + echo -e "\e[31m$(date) - Dovecot hit error limit\e[0m" + echo dovecot-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! dns_checks; then + log_to_redis "Unbound hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "unbound-mailcow" + echo -e "\e[31m$(date) - Unbound hit error limit\e[0m" + #echo unbound-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + if ! rspamd_checks; then + log_to_redis "Rspamd hit error limit" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "rspamd-mailcow" + echo -e "\e[31m$(date) - Rspamd hit error limit\e[0m" + echo rspamd-mailcow > /tmp/com_pipe + fi +done +) & +BACKGROUND_TASKS+=($!) + +# Monitor watchdog agents, stop script when agents fails and wait for respawn by Docker (restart:always:n) +( +while true; do + for bg_task in ${BACKGROUND_TASKS[*]}; do + if ! kill -0 ${bg_task} 1>&2; then + echo "Worker ${bg_task} died, stopping watchdog and waiting for respawn..." + log_to_redis "Worker ${bg_task} died, stopping watchdog and waiting for respawn..." + kill -TERM 1 + fi + sleep 10 + done +done +) & + +# Restart container when threshold limit reached +while true; do + CONTAINER_ID= + read com_pipe_answer hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { - if (in_net($_GET['host'], $host)) { - echo '200 PERMIT'; - exit; +if (isset($_GET['host'])) { + try { + foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { + if (in_net($_GET['host'], $host)) { + echo '200 PERMIT'; + exit; + } + } + echo '200 DUNNO'; + } + catch (RedisException $e) { + echo '200 DUNNO'; + exit; + } +} else { + try { + foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { + echo $host . "\n"; } } - echo '200 DUNNO'; -} -catch (RedisException $e) { - echo '200 DUNNO'; - exit; + catch (RedisException $e) { + exit; + } } ?> diff --git a/data/conf/rspamd/dynmaps/index.html b/data/conf/rspamd/dynmaps/index.html new file mode 100644 index 00000000..90531a4b --- /dev/null +++ b/data/conf/rspamd/dynmaps/index.html @@ -0,0 +1,2 @@ + + diff --git a/data/conf/rspamd/dynmaps/settings.php b/data/conf/rspamd/dynmaps/settings.php index 552918e1..335c0c66 100644 --- a/data/conf/rspamd/dynmaps/settings.php +++ b/data/conf/rspamd/dynmaps/settings.php @@ -97,6 +97,18 @@ function ucl_rcpts($object, $type) { } ?> settings { + watchdog { + priority = 10; + rcpt = "/null@localhost/i"; + from = "/watchdog@localhost/i"; + apply "default" { + actions { + reject = 9999.0; + greylist = 9998.0; + "add header" = 9997.0; + } + } + } SOGo
  • Fail2ban
  • Rspamd
  • +
  • Autodiscover
  • @@ -128,6 +129,7 @@ $tfa_data = get_tfa(); Relayhosts + @@ -149,10 +151,10 @@ $tfa_data = get_tfa();
    -

    Domain:
    - - Selector '' - bit +

    Domain: +

    +

    Selector ''

    +

    bit

    @@ -179,10 +181,10 @@ $tfa_data = get_tfa();
    -

    ↳ Alias-Domain:
    - - Selector '' - bit +

    ↳ Alias-Domain: +

    +

    Selector ''

    +

    bit

    @@ -211,10 +213,10 @@ $tfa_data = get_tfa();
    -

    Domain:
    - - Selector '' - bit +

    Domain: +

    +

    Selector ''

    +

    bit

    @@ -259,9 +261,7 @@ $tfa_data = get_tfa();
    - +
    @@ -375,6 +375,82 @@ XYZ
    + + +
    +
    +
    + +

    +
    +

    + + +

    +
    + +
    +
    +
    + mailcow logo +
    + x px + + +
    +
    +
    +
    +

    +
    +
    +
    + + +

    +
    + + + + + + + $val): + ?> + + + + + + + + + + + + + +
    + + +
    +
    +
    +
    @@ -469,6 +545,24 @@ XYZ +
    +
    +
    Autodiscover +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index 551cc6dc..5e0ccf30 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -15,14 +15,13 @@ error_reporting(0); $data = trim(file_get_contents("php://input")); -// Desktop client needs IMAP, unless it's Outlook 2013 or higher on Windows -if (strpos($data, 'autodiscover/outlook/responseschema') !== false) { // desktop client +if (strpos($data, 'autodiscover/outlook/responseschema') !== false) { $autodiscover_config['autodiscoverType'] = 'imap'; if ($autodiscover_config['useEASforOutlook'] == 'yes' && // Office for macOS does not support EAS strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && // Outlook 2013 (version 15) or higher - preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT']) + preg_match('/(Outlook|Office).+15\./', $_SERVER['HTTP_USER_AGENT']) ) { $autodiscover_config['autodiscoverType'] = 'activesync'; } @@ -39,7 +38,26 @@ $login_user = strtolower(trim($_SERVER['PHP_AUTH_USER'])); $login_role = check_login($login_user, $_SERVER['PHP_AUTH_PW']); if (!isset($_SERVER['PHP_AUTH_USER']) OR $login_role !== "user") { - header('WWW-Authenticate: Basic realm=""'); + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => "none", + "service" => "Error: must be authenticated" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"'); header('HTTP/1.0 401 Unauthorized'); exit(0); } @@ -52,6 +70,25 @@ else { time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $_SERVER['PHP_AUTH_USER'], + "service" => "Error: invalid or missing request data" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } list($usec, $sec) = explode(' ', microtime()); ?> @@ -82,12 +119,30 @@ else { die("Failed to determine name from SQL"); } if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); + $displayname = $MailboxData['name']; } else { $displayname = $email; } - + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $_SERVER['PHP_AUTH_USER'], + "service" => $autodiscover_config['autodiscoverType'] + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } if ($autodiscover_config['autodiscoverType'] == 'imap') { ?> @@ -121,13 +176,13 @@ else { CalDAV - https:///SOGo/dav//Calendar + https:///SOGo/dav// off CardDAV - https:///SOGo/dav//Contacts + https:///SOGo/dav// off diff --git a/data/web/css/admin.css b/data/web/css/admin.css index aa07d7a3..eb6b64f8 100644 --- a/data/web/css/admin.css +++ b/data/web/css/admin.css @@ -53,3 +53,7 @@ body.modal-open { top: 65px; z-index: 1; } +.thumbnail img { + min-height:100px; + height:100px; +} \ No newline at end of file diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css index eeade3c7..955b05fb 100644 --- a/data/web/css/mailcow.css +++ b/data/web/css/mailcow.css @@ -83,6 +83,9 @@ body.modal-open { overflow: inherit; padding-right: inherit !important; } +body { + font-size:11pt; +} #mailcow-alert { position: fixed; bottom: 8px; @@ -98,4 +101,13 @@ legend { -ms-user-select: none -o-user-select: none; user-select: none; +} +.navbar .navbar-brand { + padding-top: 5px; +} +.navbar .navbar-brand img { + height: 40px; +} +.mailcow-logo img { + max-width: 250px; } \ No newline at end of file diff --git a/data/web/edit.php b/data/web/edit.php index fd6682ff..ea9424e9 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -457,6 +457,23 @@ if (isset($_SESSION['mailcow_cc_role'])) { + +
    + +
    + +
    +
    diff --git a/data/web/inc/call_sogo_ctrl.php b/data/web/inc/call_sogo_ctrl.php index 990defa2..c54a0a5a 100644 --- a/data/web/inc/call_sogo_ctrl.php +++ b/data/web/inc/call_sogo_ctrl.php @@ -5,36 +5,117 @@ if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role echo "Not allowed." . PHP_EOL; exit(); } -if ($_GET['ACTION'] == "start") { - $request = xmlrpc_encode_request("supervisor.startProcess", 'bootstrap-sogo', array('encoding'=>'utf-8')); - $context = stream_context_create(array('http' => array( - 'method' => "POST", - 'header' => "Content-Length: " . strlen($request), - 'content' => $request - ))); - $file = @file_get_contents("http://sogo:9191/RPC2", false, $context) or die("Cannot connect to $remote_server:$listener_port"); - $response = xmlrpc_decode($file); - if (isset($response['faultString'])) { - echo '' . $response['faultString'] . ''; - } - else { - echo 'OK'; + +function docker($service_name, $action, $post_action = null, $post_fields = null) { + $curl = curl_init(); + curl_setopt($curl, CURLOPT_HTTPHEADER,array( 'Content-Type: application/json' )); + switch($action) { + case 'get_id': + curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/json'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_POST, 0); + $response = curl_exec($curl); + if ($response === false) { + $err = curl_error($curl); + curl_close($curl); + return $err; + } + else { + curl_close($curl); + $containers = json_decode($response, true); + if (!empty($containers)) { + foreach ($containers as $container) { + if ($container['Config']['Labels']['com.docker.compose.service'] == $service_name) { + return trim($container['Id']); + } + } + } + } + return false; + break; + case 'info': + $container_id = docker($service_name, 'get_id'); + if (ctype_xdigit($container_id)) { + curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/json'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_POST, 0); + $response = curl_exec($curl); + if ($response === false) { + $err = curl_error($curl); + curl_close($curl); + return $err; + } + else { + curl_close($curl); + if (empty($response)) { + return true; + } + else { + return json_decode($response, true); + } + } + } + else { + return false; + } + break; + case 'post': + if (!empty($post_action)) { + $container_id = docker($service_name, 'get_id'); + if (ctype_xdigit($container_id) && ctype_alnum($post_action)) { + curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $post_action); + curl_setopt($curl, CURLOPT_POST, 1); + if (!empty($post_fields)) { + curl_setopt( $curl, CURLOPT_POSTFIELDS, json_encode($post_fields)); + } + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + $response = curl_exec($curl); + if ($response === false) { + $err = curl_error($curl); + curl_close($curl); + return $err; + } + else { + curl_close($curl); + if (empty($response)) { + return true; + } + else { + return $response; + } + } + } + } + break; } } -elseif ($_GET['ACTION'] == "stop") { - $request = xmlrpc_encode_request("supervisor.stopProcess", 'bootstrap-sogo', array('encoding'=>'utf-8')); - $context = stream_context_create(array('http' => array( - 'method' => "POST", - 'header' => "Content-Length: " . strlen($request), - 'content' => $request - ))); - $file = @file_get_contents("http://sogo:9191/RPC2", false, $context) or die("Cannot connect to $remote_server:$listener_port"); - $response = xmlrpc_decode($file); - if (isset($response['faultString'])) { - echo '' . $response['faultString'] . ''; - } - else { - echo 'OK'; - } + +if ($_GET['ACTION'] == "start") { + $retry = 0; + while (docker('sogo-mailcow', 'info')['State']['Running'] != 1 && $retry <= 3) { + $response = docker('sogo-mailcow', 'post', 'start'); + $last_response = (trim($response) == "\"OK\"") ? 'OK' : 'Error: ' . $response . ''; + if (trim($response) == "\"OK\"") { + break; + } + usleep(1500000); + $retry++; + } + echo (!isset($last_response)) ? 'Already running' : $last_response; } + +if ($_GET['ACTION'] == "stop") { + $retry = 0; + while (docker('sogo-mailcow', 'info')['State']['Running'] == 1 && $retry <= 3) { + $response = docker('sogo-mailcow', 'post', 'stop'); + $last_response = (trim($response) == "\"OK\"") ? 'OK' : 'Error: ' . $response . ''; + if (trim($response) == "\"OK\"") { + break; + } + usleep(1500000); + $retry++; + } + echo (!isset($last_response)) ? 'Not running' : $last_response; +} + ?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 55a8f88d..8740612c 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -6,10 +6,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/footer.php'; + diff --git a/data/web/inc/functions.autoconfiguration.inc.php b/data/web/inc/functions.autoconfiguration.inc.php new file mode 100644 index 00000000..772475c7 --- /dev/null +++ b/data/web/inc/functions.autoconfiguration.inc.php @@ -0,0 +1,119 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + switch ($_type) { + case 'autodiscover': + $objects = (array)$_data['object']; + foreach ($objects as $object) { + if (is_valid_domain_name($object) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) { + $exclude_regex = (isset($_data['exclude_regex'])) ? $_data['exclude_regex'] : null; + $exclude_regex = (isset($_data['exclude_regex'])) ? $_data['exclude_regex'] : null; + try { + $stmt = $pdo->prepare("SELECT COUNT(`domain`) AS `domain_c` FROM `autodiscover` + WHERE `domain` = :domain"); + $stmt->execute(array(':domain' => $object)); + $num_results = $stmt->fetchColumn(); + if ($num_results > 0) { + $stmt = $pdo->prepare("SELECT COUNT(`domain`) AS `domain_c` FROM `autodiscover` + WHERE `domain` = :domain"); + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + elseif (filter_var($object, FILTER_VALIDATE_EMAIL) === true && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) { + + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars(implode(', ', $objects))) + ); + break; + } + break; + case 'get': + switch ($_type) { + case 'autodiscover': + $autodiscover = array(); + if (is_valid_domain_name($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { + try { + $stmt = $pdo->prepare("SELECT * FROM `autodiscover` + WHERE `domain` = :domain"); + $stmt->execute(array(':domain' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $autodiscover['mailbox'] = $row['mailbox']; + $autodiscover['domain'] = $row['domain']; + $autodiscover['service'] = $row['service']; + $autodiscover['exclude_regex'] = $row['exclude_regex']; + $autodiscover['created'] = $row['created']; + $autodiscover['modified'] = $row['modified']; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + elseif (filter_var($_data, FILTER_VALIDATE_EMAIL) === true && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { + try { + $stmt = $pdo->prepare("SELECT * FROM `autodiscover` + WHERE `mailbox` = :mailbox"); + $stmt->execute(array(':mailbox' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $autodiscover['mailbox'] = $row['mailbox']; + $autodiscover['domain'] = $row['domain']; + $autodiscover['service'] = $row['service']; + $autodiscover['exclude_regex'] = $row['exclude_regex']; + $autodiscover['created'] = $row['created']; + $autodiscover['modified'] = $row['modified']; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + return $autodiscover; + break; + } + break; + case 'reset': + switch ($_type) { + case 'autodiscover': + if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { + return false; + } + break; + } + break; + } +} +$miau = "Microsoft Office/15.0 (Windows NT 5.1; macOS Outlook 16.0.4734; Pro)"; +preg_match("/^((?!.*Mac|.*emClient).)*(Outlook|Office).+1[5-9].*/i", $miau, $output_array); +if (empty($output_array)) { + echo "imap"; +} \ No newline at end of file diff --git a/data/web/inc/functions.customize.inc.php b/data/web/inc/functions.customize.inc.php new file mode 100644 index 00000000..231bc898 --- /dev/null +++ b/data/web/inc/functions.customize.inc.php @@ -0,0 +1,180 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + switch ($_item) { + case 'main_logo': + if (in_array($_data['main_logo']['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) { + try { + if (file_exists($_data['main_logo']['tmp_name']) !== true) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Cannot validate image file: Temporary file not found' + ); + return false; + } + $image = new Imagick($_data['main_logo']['tmp_name']); + if ($image->valid() !== true) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Cannot validate image file' + ); + return false; + } + $image->destroy(); + } + catch (ImagickException $e) { + $image->destroy(); + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Cannot validate image file' + ); + return false; + } + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid mime type' + ); + return false; + } + try { + $redis->Set('MAIN_LOGO', 'data:' . $_data['main_logo']['type'] . ';base64,' . base64_encode(file_get_contents($_data['main_logo']['tmp_name']))); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'File uploaded successfully' + ); + break; + } + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + switch ($_item) { + case 'app_links': + $apps = (array)$_data['app']; + $links = (array)$_data['href']; + $out = array(); + if (count($apps) == count($links)) {; + for ($i = 0; $i < count($apps); $i++) { + $out[] = array($apps[$i] => $links[$i]); + } + try { + $redis->set('APP_LINKS', json_encode($out)); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Saved changes to app links' + ); + break; + } + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + switch ($_item) { + case 'main_logo': + try { + if ($redis->del('MAIN_LOGO')) { + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Reset default logo' + ); + return true; + } + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + break; + } + break; + case 'get': + switch ($_item) { + case 'app_links': + try { + $app_links = json_decode($redis->get('APP_LINKS'), true); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + return ($app_links) ? $app_links : false; + break; + case 'main_logo': + try { + return $redis->get('MAIN_LOGO'); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + break; + case 'main_logo_specs': + try { + $image = new Imagick(); + $img_data = explode('base64,', customize('get', 'main_logo')); + if ($img_data[1]) { + $image->readImageBlob(base64_decode($img_data[1])); + } + return $image->identifyImage(); + } + catch (ImagickException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Error: Imagick exception while reading image' + ); + return false; + } + break; + } + break; + } +} \ No newline at end of file diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 79f3c6dc..79a449eb 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -244,6 +244,23 @@ function set_acl() { return false; } } +function get_acl($username) { + global $pdo; + if ($_SESSION['mailcow_cc_role'] != "admin") { + return false; + } + $username = strtolower(trim($username)); + $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $acl = $stmt->fetch(PDO::FETCH_ASSOC); + unset($acl['username']); + if (!empty($acl)) { + return $acl; + } + else { + return false; + } +} function formatBytes($size, $precision = 2) { if(!is_numeric($size)) { return "0"; @@ -900,6 +917,14 @@ function get_logs($container, $lines = 100) { return $data_array; } } + if ($container == "autodiscover-mailcow") { + if ($data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines)) { + foreach ($data as $json_line) { + $data_array[] = json_decode($json_line, true); + } + return $data_array; + } + } if ($container == "rspamd-history") { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history"); diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 1e674e44..209b4acc 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -755,7 +755,7 @@ function mailbox($_action, $_type, $_data = null) { ':active' => $active )); $stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`) - VALUES (:username, '0', '0')"); + VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';"); $stmt->execute(array(':username' => $username)); $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`) VALUES (:username1, :username2, :domain, :active)"); @@ -1291,11 +1291,11 @@ function mailbox($_action, $_type, $_data = null) { $port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1']; $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1']; $host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1']; - $subfolder2 = (!empty($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2']; + $subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2']; $enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1']; $mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval']; - $exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude']; - $maxage = (!empty($_data['maxage'])) ? $_data['maxage'] : $is_now['maxage']; + $exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : ''; + $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage']; } else { $_SESSION['return'] = array( diff --git a/data/web/inc/functions.policy.inc.php b/data/web/inc/functions.policy.inc.php index 2de1c6e2..9609d5e1 100644 --- a/data/web/inc/functions.policy.inc.php +++ b/data/web/inc/functions.policy.inc.php @@ -32,7 +32,7 @@ function policy($_action, $_scope, $_data = null) { $object_list = "whitelist_from"; } $object_from = preg_replace('/\.+/', '.', rtrim(preg_replace("/\.\*/", "*", trim(strtolower($_data['object_from']))), '.')); - if (!ctype_alnum(str_replace(array('@', '.', '-', '*'), '', $object_from))) { + if (!ctype_alnum(str_replace(array('@', '_', '.', '-', '*'), '', $object_from))) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['policy_list_from_invalid']) @@ -112,7 +112,7 @@ function policy($_action, $_scope, $_data = null) { $object_list = "whitelist_from"; } $object_from = preg_replace('/\.+/', '.', rtrim(preg_replace("/\.\*/", "*", trim(strtolower($_data['object_from']))), '.')); - if (!ctype_alnum(str_replace(array('@', '.', '-', '*'), '', $object_from))) { + if (!ctype_alnum(str_replace(array('@', '_', '.', '-', '*'), '', $object_from))) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['policy_list_from_invalid']) diff --git a/data/web/inc/functions.relayhost.inc.php b/data/web/inc/functions.relayhost.inc.php index 249dacc3..233ca0ad 100644 --- a/data/web/inc/functions.relayhost.inc.php +++ b/data/web/inc/functions.relayhost.inc.php @@ -27,7 +27,7 @@ function relayhost($_action, $_data = null) { $stmt->execute(array( ':hostname' => $hostname, ':username' => $username, - ':password' => $password, + ':password' => str_replace(':', '\:', $password), ':active' => '1' )); } diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index ccccc616..b64bd5a8 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -39,7 +39,7 @@ - mailcow-logo + mailcow-logo