Bläddra i källkod

Merge branch 'master' of https://github.com/mailcow/mailcow-dockerized

andryyy 6 år sedan
förälder
incheckning
9f2a6f13a5
3 ändrade filer med 233 tillägg och 164 borttagningar
  1. 5 3
      data/Dockerfiles/acme/Dockerfile
  2. 177 139
      data/Dockerfiles/acme/docker-entrypoint.sh
  3. 51 22
      data/web/lang/lang.nl.php

+ 5 - 3
data/Dockerfiles/acme/Dockerfile

@@ -1,10 +1,9 @@
-FROM alpine:3.8
+FROM alpine:3.9
 
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 RUN apk add --update --no-cache \
   bash \
-  acme-client \
   curl \
   openssl \
   bind-tools \
@@ -12,7 +11,10 @@ RUN apk add --update --no-cache \
   mariadb-client \
   redis \
   tini \
-  tzdata
+  tzdata \
+  py-pip \
+  && pip install --upgrade pip \
+  && pip install acme-tiny
 
 COPY docker-entrypoint.sh /srv/docker-entrypoint.sh
 COPY expand6.sh /srv/expand6.sh

+ 177 - 139
data/Dockerfiles/acme/docker-entrypoint.sh

@@ -17,7 +17,7 @@ log_f() {
     redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${1}")\"}" > /dev/null
   else
     redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
-      tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null
+      tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null
   fi
 }
 
@@ -36,7 +36,12 @@ log_f "OK" no_date
 ACME_BASE=/var/lib/acme
 SSL_EXAMPLE=/var/lib/ssl-example
 
-mkdir -p ${ACME_BASE}/acme/private
+mkdir -p ${ACME_BASE}/acme
+
+# Migrate
+[[ -f ${ACME_BASE}/acme/private/privkey.pem ]] && mv ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/key.pem
+[[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/account.pem
+
 
 reload_configurations(){
   # Reading container IDs
@@ -112,6 +117,19 @@ get_ipv6(){
   echo ${IPV6}
 }
 
+verify_challenge_path(){
+  # verify_challenge_path URL 4|6
+  RAND_FILE=${RANDOM}${RANDOM}${RANDOM}
+  touch /var/www/acme/${RAND_FILE}
+  if [[ "$(curl -${2} http://${1}/.well-known/acme-challenge/${RAND_FILE} --write-out %{http_code} --silent --output /dev/null)" == "200" ]]; then
+    rm /var/www/acme/${RAND_FILE}
+    return 0
+  else
+    rm /var/www/acme/${RAND_FILE}
+    return 1
+  fi
+}
+
 [[ ! -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
@@ -120,20 +138,13 @@ if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; 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
+  if [[ -f ${ACME_BASE}/acme/cert.pem ]] && [[ -f ${ACME_BASE}/acme/key.pem ]]; then
+    if verify_hash_match ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/key.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
+      cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem
+      cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem
       # Restarting with env var set to trigger a restart,
       exec env TRIGGER_RESTART=1 $(readlink -f "$0")
     fi
@@ -150,24 +161,59 @@ log_f "Waiting for database... "
 while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
   sleep 2
 done
-log_f "Initializing, please wait... "
 
+# Waiting for domain table
+log_f "Waiting for domain table... " no_nl
+while [[ -z ${DOMAIN_TABLE} ]]; do
+  curl --silent http://nginx/ >/dev/null 2>&1
+  DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
+  [[ -z ${DOMAIN_TABLE} ]] && sleep 10
+done
+log_f "OK" no_date
+
+log_f "Initializing, please wait... "
 
 while true; do
+
+  # Re-using previous acme-mailcow account and domain keys
+  if [[ ! -f ${ACME_BASE}/acme/key.pem ]]; then
+    log_f "Generating missing domain private key..."
+    openssl genrsa 4096 > ${ACME_BASE}/acme/key.pem
+  else
+    log_f "Using existing domain key ${ACME_BASE}/acme/key.pem"
+  fi
+  if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then
+    log_f "Generating missing Lets Encrypt account key..."
+    openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem
+  else
+    log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
+  fi
+
+  # Skipping IP check when we like to live dangerously
   if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
     SKIP_IP_CHECK=y
   fi
+
+  # Cleaning up and init validation arrays
   unset SQL_DOMAIN_ARR
   unset VALIDATED_CONFIG_DOMAINS
   unset ADDITIONAL_VALIDATED_SAN
+  unset ADDITIONAL_WC_ARR
+  unset ADDITIONAL_SAN_ARR
+  unset SAN_CHANGE
+  unset SAN_ARRAY_NOW
+  unset ORPHANED_SAN
+  unset ADDED_SAN
+  SAN_CHANGE=0
+  declare -a SAN_ARRAY_NOW
+  declare -a ORPHANED_SAN
+  declare -a ADDED_SAN
   declare -a SQL_DOMAIN_ARR
   declare -a VALIDATED_CONFIG_DOMAINS
   declare -a ADDITIONAL_VALIDATED_SAN
+  declare -a ADDITIONAL_WC_ARR
+  declare -a ADDITIONAL_SAN_ARR
   IFS=',' read -r -a TMP_ARR <<< "${ADDITIONAL_SAN}"
-  log_f "Detecting IP addresses... " no_nl
-
-  unset ADDITIONAL_WC_ARR
-  unset ADDITIONAL_SAN_ARR
   for i in "${TMP_ARR[@]}" ; do
     if [[ "$i" =~ \.\*$ ]]; then
       ADDITIONAL_WC_ARR+=(${i::-2})
@@ -177,6 +223,8 @@ while true; do
   done
   ADDITIONAL_WC_ARR+=('autodiscover')
 
+  # Start IP detection
+  log_f "Detecting IP addresses... " no_nl
   IPV4=$(get_ipv4)
   IPV6=$(get_ipv6)
   log_f "OK" no_date
@@ -194,23 +242,15 @@ while true; do
     fi
   fi
 
-  log_f "Waiting for domain table... " no_nl
-  while [[ -z ${DOMAIN_TABLE} ]]; do
-    curl --silent http://nginx/ >/dev/null 2>&1
-    DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
-    [[ -z ${DOMAIN_TABLE} ]] && sleep 10
-  done
-  log_f "OK" no_date
-
+  #########################################
+  # IP and webroot challenge verification #
   while read domains; do
     SQL_DOMAIN_ARR+=("${domains}")
   done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs)
 
   for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
     for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
-      if [[  "${SUBDOMAIN}.${SQL_DOMAIN}" == ${MAILCOW_HOSTNAME} ]]; then
-        log_f "Skipping mailcow hostname (${MAILCOW_HOSTNAME}), will be added anyway"
-      else
+      if [[  "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
         A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
         AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
         # Check if CNAME without v6 enabled target
@@ -220,16 +260,24 @@ while true; do
         if [[ ! -z ${AAAA_SUBDOMAIN} ]]; then
           log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check"
           if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
-            log_f "Confirmed AAAA record ${SUBDOMAIN}.${SQL_DOMAIN}"
-            VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
+            if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 6; then
+              log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}"
+              VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
+            else
+              log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}, but HTTP validation failed"
+            fi
           else
             log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))"
           fi
         elif [[ ! -z ${A_SUBDOMAIN} ]]; then
           log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}"
           if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
-            log_f "Confirmed A record ${SUBDOMAIN}.${SQL_DOMAIN}"
-            VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
+            if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 4; then
+              log_f "Confirmed A record ${A_SUBDOMAIN}"
+              VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
+            else
+              log_f "Confirmed AAAA record ${A_SUBDOMAIN}, but HTTP validation failed"
+            fi
           else
             log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})"
           fi
@@ -249,16 +297,24 @@ while true; do
   if [[ ! -z ${AAAA_MAILCOW_HOSTNAME} ]]; then
     log_f "Found AAAA record for ${MAILCOW_HOSTNAME}: ${AAAA_MAILCOW_HOSTNAME} - skipping A record check"
     if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_MAILCOW_HOSTNAME}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
-      log_f "Confirmed AAAA record ${MAILCOW_HOSTNAME}"
-      VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+      if verify_challenge_path "${MAILCOW_HOSTNAME}" 6; then
+        log_f "Confirmed AAAA record ${AAAA_MAILCOW_HOSTNAME}"
+        VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+      else
+        log_f "Confirmed AAAA record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
+      fi
     else
       log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} ($(expand ${AAAA_MAILCOW_HOSTNAME}))"
     fi
   elif [[ ! -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 ${A_MAILCOW_HOSTNAME}"
-      VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+      if verify_challenge_path "${MAILCOW_HOSTNAME}" 4; then
+        log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}"
+        VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+      else
+        log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
+      fi
     else
       log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME})"
     fi
@@ -290,16 +346,24 @@ while true; do
     if [[ ! -z ${AAAA_SAN} ]]; then
       log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check"
       if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
-        log_f "Confirmed AAAA record ${SAN}"
-        ADDITIONAL_VALIDATED_SAN+=("${SAN}")
+        if verify_challenge_path "${SAN}" 6; then
+          log_f "Confirmed AAAA record ${AAAA_SAN}"
+          ADDITIONAL_VALIDATED_SAN+=("${SAN}")
+        else
+          log_f "Confirmed AAAA record ${AAAA_SAN}, but HTTP validation failed"
+        fi
       else
         log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} ($(expand ${AAAA_SAN}))"
       fi
     elif [[ ! -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 ${A_SAN}"
-        ADDITIONAL_VALIDATED_SAN+=("${SAN}")
+        if verify_challenge_path "${SAN}" 4; then
+          log_f "Confirmed A record ${A_SAN}"
+          ADDITIONAL_VALIDATED_SAN+=("${SAN}")
+        else
+          log_f "Confirmed A record ${A_SAN}, but HTTP validation failed"
+        fi
       else
         log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (${A_SAN})"
       fi
@@ -317,123 +381,97 @@ while true; do
     exec $(readlink -f "$0")
   fi
 
+  # Collecting SANs from active certificate
+  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}
+  fi
+
+  # Finding difference in SAN array now vs. SAN array by current configuration
   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
+  if [[ ! -z ${ORPHANED_SAN[*]} ]]; then
+    log_f "Found orphaned SANs ${ORPHANED_SAN[*]}"
+    SAN_CHANGE=1
+  fi
+  array_diff ADDED_SAN ALL_VALIDATED SAN_ARRAY_NOW
+  if [[ ! -z ${ADDED_SAN[*]} ]]; then
+    log_f "Found new SANs ${ADDED_SAN[*]}"
+    SAN_CHANGE=1
   fi
 
+  if [[ ${SAN_CHANGE} == 0 ]]; then
+    # Certificate did not change but could be due for renewal (4 weeks)
+    if ! openssl x509 -checkend 1209600 -noout -in ${ACME_BASE}/cert.pem; then
+      log_f "Certificate is due for renewal (< 2 weeks)"
+    else
+      log_f "Certificate validation done, neither changed nor due for renewal, sleeping for another day."
+      sleep 1d
+      continue
+    fi
+  fi
+
+  DATE=$(date +%Y-%m-%d_%H_%M_%S)
+  log_f "Creating backups in ${ACME_BASE}/backups/${DATE}/ ..."
+  mkdir -p ${ACME_BASE}/backups/${DATE}/
+  [[ -f ${ACME_BASE}/acme/acme.csr ]] && cp ${ACME_BASE}/acme/acme.csr ${ACME_BASE}/backups/${DATE}/
+  [[ -f ${ACME_BASE}/acme/cert.pem ]] && cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/backups/${DATE}/
+  [[ -f ${ACME_BASE}/acme/key.pem ]] && cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/backups/${DATE}/
+  [[ -f ${ACME_BASE}/acme/account.pem ]] && cp ${ACME_BASE}/acme/account.pem ${ACME_BASE}/backups/${DATE}/
+
+  # Generating CSR
+  printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
+  printf "DNS:%s," "${ALL_VALIDATED[@]}" >> /tmp/_SAN
+  sed -i '$s/,$//' /tmp/_SAN
+  openssl req -new -sha256 -key ${ACME_BASE}/acme/key.pem -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf /tmp/_SAN) > ${ACME_BASE}/acme/acme.csr
+
   if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
     log_f "Using Let's Encrypt staging servers"
-    STAGING_PARAMETER="-s"
+    STAGING_PARAMETER='--directory-url https://acme-staging-v02.api.letsencrypt.org/directory'
   else
     STAGING_PARAMETER=
   fi
 
-  ACME_RESPONSE=$(acme-client \
-    -v -e -b -N -n ${STAGING_PARAMETER} \
-    -a 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf' \
-    -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)
+  # acme-tiny writes info to stderr and ceritifcate to stdout
+  # The redirects will do the following:
+  # - redirect stdout to temp certificate file
+  # - redirect acme-tiny stderr to stdout (logs to variable ACME_RESPONSE)
+  # - tee stderr to get live output and log to dockerd
+
+  ACME_RESPONSE=$(acme-tiny ${STAGING_PARAMETER} \
+    --account-key ${ACME_BASE}/acme/account.pem \
+    --disable-check \
+    --csr ${ACME_BASE}/acme/acme.csr \
+    --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5)
+
   case "$?" in
-    0) # new certs
-      ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64)
+    0) # cert requested
+      ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
       log_f "${ACME_RESPONSE_B64}" redis_only b64
-      # 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
-        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
-      reload_configurations
-      ;;
-    1) # failure
-      ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64)
-      log_f "${ACME_RESPONSE_B64}" redis_only b64
-      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
+      log_f "Deploying..."
+      # Deploy the new certificate and key
+      # Moving temp cert to acme/cert.pem
+      if verify_hash_match /tmp/_cert.pem ${ACME_BASE}/acme/key.pem; then
+        mv /tmp/_cert.pem ${ACME_BASE}/acme/cert.pem
+        cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem
+        cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem
+        reload_configurations
+        rm /var/www/acme/*
+        log_f "Certificate successfully deployed, removing backup, sleeping 1d"
+        sleep 1d
+      else
+        log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, ignoring certificate"
+        log_f "Retrying in 30 minutes..."
+        sleep 30m
         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 ]] && reload_configurations
-      log_f "Retrying in 30 minutes..."
-      sleep 30m
-      exec $(readlink -f "$0")
-      ;;
-    2) # no change
-      ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64)
-      log_f "${ACME_RESPONSE_B64}" redis_only b64
-      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
-      log_f "Certificate was not changed"
-      [[ ${TRIGGER_RESTART} == 1 ]] && reload_configurations
       ;;
-    *) # unspecified
-      ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64)
+    *) # non-zero is non-fun
+      ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
       log_f "${ACME_RESPONSE_B64}" redis_only b64
-      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..."
-        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 ]] && reload_configurations
       log_f "Retrying in 30 minutes..."
       sleep 30m
       exec $(readlink -f "$0")
       ;;
   esac
 
-  log_f "ACME certificate validation done. Sleeping for another day."
-  sleep 1d
-
 done

+ 51 - 22
data/web/lang/lang.nl.php

@@ -46,7 +46,7 @@ $lang['danger']['malformed_username'] = "Ongeldige gebruikersnaam";
 $lang['info']['awaiting_tfa_confirmation'] = "In afwachting van tweefactorauthenticatie...";
 $lang['success']['logged_in_as'] = "Succesvol ingelogd als %s";
 $lang['danger']['login_failed'] = "Aanmelding mislukt";
-$lang['danger']['set_acl_failed'] = "ALC kon niet worden ingesteld";
+$lang['danger']['set_acl_failed'] = "Toegangscontrole kon niet worden ingesteld";
 $lang['danger']['no_user_defined'] = "Geen gebruiker gespecificeerd";
 $lang['danger']['script_empty'] = "Script dient ingevuld te worden";
 $lang['danger']['sieve_error'] = "Sieve-fout: %s";
@@ -56,7 +56,7 @@ $lang['danger']['domain_cannot_match_hostname'] = "Het domein dient af te wijken
 $lang['warning']['domain_added_sogo_failed'] = "Domein is toegevoegd, maar het hestarten van SOGo mislukte. Controleer de serverlogs.";
 $lang['danger']['rl_timeframe'] = "Ratelimit-tijdsbestek is ongeldig";
 $lang['success']['rl_saved'] = "Ratelimit voor object %s is opgeslagen";
-$lang['success']['acl_saved'] = "ACL voor object %s is opgeslagen";
+$lang['success']['acl_saved'] = "Toegangscontrole voor object %s is opgeslagen";
 $lang['success']['deleted_syncjobs'] = "Synchronisatietaken %s zijn verwijderd";
 $lang['success']['deleted_syncjob'] = "Synchronisatietaak %s is verwijderd";
 $lang['success']['delete_filters'] = "Filters %s zijn verwijderd";
@@ -154,7 +154,7 @@ $lang['danger']['max_quota_in_use'] = "Postvakquotum moet gelijk zijn aan, of gr
 $lang['danger']['domain_quota_m_in_use'] = "Domeinquotum moet gelijk zijn aan, of groter zijn dan %s MiB";
 $lang['danger']['mailboxes_in_use'] = "Maximaal aantal postvakken moet gelijk zijn aan, of groter zijn dan %d";
 $lang['danger']['aliases_in_use'] = "Maximaal aantal aliassen moet gelijk zijn aan, of groter zijn dan %d";
-$lang['danger']['sender_acl_invalid'] = "ACL-waarde van afzender %s is ongeldig";
+$lang['danger']['sender_acl_invalid'] = "Toegangscontrole van afzender %s is ongeldig";
 $lang['danger']['domain_not_empty'] = "Kan geen domein in gebruik verwijderen";
 $lang['danger']['validity_missing'] = 'Wijs een geldigheidstermijn toe';
 $lang['user']['loading'] = "Bezig met laden...";
@@ -229,12 +229,12 @@ $lang['user']['tag_handling'] = 'Omgaan met e-mailtags';
 $lang['user']['tag_in_subfolder'] = 'In submap';
 $lang['user']['tag_in_subject'] = 'In onderwerp';
 $lang['user']['tag_in_none'] = 'Niets doen';
-$lang['user']['tag_help_explain'] = 'In submap: er wordt een nieuwe map aangemaakt, genoemd naar de tag (bijv.: "INBOX/Apple").<br>In onderwerp: de tag wordt vóór het oorspronkelijke onderwerp geplaatst (bijv.: "[Apple] Uw bestelling").';
-$lang['user']['tag_help_example'] = 'Voorbeeld van een e-maildres met tag: ik<b>+Apple</b>@example.org';
+$lang['user']['tag_help_explain'] = 'In submap: er wordt een nieuwe map aangemaakt, genoemd naar de tag (bijv.: "INBOX/Tesla").<br>In onderwerp: de tag wordt vóór het oorspronkelijke onderwerp geplaatst (bijv.: "[Tesla] Uw serviceafspraak").';
+$lang['user']['tag_help_example'] = 'Voorbeeld van een e-maildres met tag: ik<b>+Tesla</b>@example.org';
 
 $lang['user']['eas_reset'] = 'Herstel ActiveSync-apparaatcache';
 $lang['user']['eas_reset_now'] = 'Herstel nu';
-$lang['user']['eas_reset_help'] = 'In de meeste gevallen verhelpt dit problemen met ActiveSync op uw apparaten<br><b>Let wel:</b> alle onderdelen zullen opnieuw gedownload moeten worden!';
+$lang['user']['eas_reset_help'] = 'In de meeste gevallen verhelpt dit problemen met ActiveSync op je apparaten<br><b>Let wel:</b> alle onderdelen zullen opnieuw gedownload moeten worden!';
 
 $lang['user']['sogo_profile_reset'] = 'Herstel SOGo-profiel';
 $lang['user']['sogo_profile_reset_now'] = 'Herstel nu';
@@ -251,9 +251,9 @@ $lang['user']['edit'] = 'Wijzig';
 $lang['user']['remove'] = 'Verwijder';
 $lang['user']['create_syncjob'] = 'Voeg een nieuwe synchronisatietaak toe';
 
-$lang['start']['mailcow_apps_detail'] = 'Gebruik een Mailcow-app om uw e-mails, agenda, contacten en meer te bekijken.';
+$lang['start']['mailcow_apps_detail'] = 'Gebruik een Mailcow-app om je e-mails, agenda, contacten en meer te bekijken.';
 $lang['start']['mailcow_panel_detail'] = '<b>Domeinbeheerders</b> kunnen postvakken en aliassen aanmaken, wijzigen en verwijderen. Ook kunnen ze domeinen aanpassen en informatie over deze verkrijgen.<br><b>Gebruikers</b> kunnen tijdelijke aliassen aanmaken, hun wachtwoord aanpassen en de spamfilterinstellingen wijzigen.';
-$lang['start']['imap_smtp_server_auth_info'] = 'Gebruik uw volledige e-mailadres en het onversleutelde verificatiemechanisme.<br>De aanmeldgegevens worden versleuteld verstuurd.';
+$lang['start']['imap_smtp_server_auth_info'] = 'Gebruik je volledige e-mailadres en het bijbehorende (onversleutelde) verificatiemechanisme.<br>De aanmeldgegevens worden versleuteld verstuurd.';
 $lang['start']['help'] = 'Toon/verberg hulppaneel';
 $lang['header']['mailcow_settings'] = 'Instellingen';
 $lang['header']['administration'] = 'Configuratie & details';
@@ -384,7 +384,14 @@ $lang['edit']['multiple_bookings'] = 'Meerdere boekingen';
 $lang['edit']['kind'] = 'Soort';
 $lang['edit']['resource'] = 'Hulpbron';
 $lang['edit']['relayhost'] = 'Afzender-afhankelijke transportkaarten';
-
+$lang['edit']['public_comment'] = 'Publiekelijke opmerking';
+$lang['mailbox']['public_comment'] = 'Publiekelijke opmerking';
+$lang['edit']['private_comment'] = 'Persoonlijke opmerking';
+$lang['mailbox']['private_comment'] = 'Persoonlijke opmerking';
+$lang['edit']['comment_info'] = 'Een persoonlijke opmerking is niet zichtbaar voor de gebruiker, terwijl een publiekelijke opmerking wel weergegeven zal worden in het overzicht van een gebruiker.';
+$lang['add']['public_comment'] = 'Publiekelijke opmerking';
+$lang['add']['private_comment'] = 'Persoonlijke opmerking';
+$lang['add']['comment_info'] = 'Een persoonlijke opmerking is niet zichtbaar voor de gebruiker, terwijl een publiekelijke opmerking wel weergegeven zal worden in het overzicht van een gebruiker.';
 $lang['acl']['spam_alias'] = 'Tijdelijke aliassen';
 $lang['acl']['tls_policy'] = 'Versleutelingsbeleid';
 $lang['acl']['spam_score'] = 'Spamscore';
@@ -393,14 +400,28 @@ $lang['acl']['delimiter_action'] = 'Delimiter-actie';
 $lang['acl']['syncjobs'] = 'Synchronisatietaken';
 $lang['acl']['eas_reset'] = 'Herstel ActiveSync-apparaatcache';
 $lang['acl']['sogo_profile_reset'] = 'Herstel SOGo-profiel';
-$lang['acl']['quarantine'] = 'Quarantaine';
+$lang['acl']['quarantine'] = 'Quarantaine-acties';
+$lang['acl']['quarantine_notification'] = 'Quarantaine-meldingen';
+$lang['acl']['quarantine_attachments'] = 'Quarantaine-bijlagen';
 $lang['acl']['alias_domains'] = 'Voeg aliasdomeinen toe';
 $lang['acl']['login_as'] = 'Log in als postvakgebruiker';
 $lang['acl']['bcc_maps'] = 'BCC-kaarten';
 $lang['acl']['filters'] = 'Filters';
 $lang['acl']['ratelimit'] = 'Ratelimit';
 $lang['acl']['recipient_maps'] = 'Ontvanger-kaarten';
-$lang['acl']['prohibited'] = 'Geweigerd door ACL';
+$lang['acl']['prohibited'] = 'Toegang geweigerd';
+
+$lang['mailbox']['quarantine_notification'] = 'Quarantaine-meldingen';
+$lang['mailbox']['never'] = 'Nooit';
+$lang['mailbox']['hourly'] = 'Ieder uur';
+$lang['mailbox']['daily'] = 'Dagelijks';
+$lang['mailbox']['weekly'] = 'Wekelijks';
+$lang['user']['quarantine_notification'] = 'Quarantaine-meldingen';
+$lang['user']['never'] = 'Nooit';
+$lang['user']['hourly'] = 'Ieder uur';
+$lang['user']['daily'] = 'Dagelijks';
+$lang['user']['weekly'] = 'Wekelijks';
+$lang['user']['quarantine_notification_info'] = 'Zodra een melding is verzonden, worden de items als gelezen gemarkeerd en zullen er geen meldingen meer over diezelfde items verstuurd worden.';
     
 $lang['add']['generate'] = 'genereer';
 $lang['add']['syncjob'] = 'Voeg een nieuwe synchronisatietaak toe';
@@ -494,11 +515,11 @@ $lang['tfa']['disable_tfa'] = "Zet TFA uit tot de eerstvolgende succesvolle logi
 $lang['tfa']['confirm'] = "Bevestig";
 $lang['tfa']['totp'] = "TOTP (Google Authenticator etc.)";
 $lang['tfa']['select'] = "Selecteer...";
-$lang['tfa']['waiting_usb_auth'] = "<i>In afwachting van USB-apparaat...</i><br><br>Druk nu op de knop van uw U2F-apparaat.";
-$lang['tfa']['waiting_usb_register'] = "<i>In afwachting van USB-apparaat...</i><br><br>Voer uw wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken.";
-$lang['tfa']['scan_qr_code'] = "Scan de volgende QR-code met uw authenticatie-app:";
-$lang['tfa']['enter_qr_code'] = "Voer deze code in als uw apparaat geen QR-codes kan scannen:";
-$lang['tfa']['confirm_totp_token'] = "Bevestig de wijzigingen door de, door uw authenticatie-app gegenereerde code, in te voeren.";
+$lang['tfa']['waiting_usb_auth'] = "<i>In afwachting van USB-apparaat...</i><br><br>Druk nu op de knop van je U2F-apparaat.";
+$lang['tfa']['waiting_usb_register'] = "<i>In afwachting van USB-apparaat...</i><br><br>Voer je wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken.";
+$lang['tfa']['scan_qr_code'] = "Scan de volgende QR-code met je authenticatie-app:";
+$lang['tfa']['enter_qr_code'] = "Voer deze code in als je apparaat geen QR-codes kan scannen:";
+$lang['tfa']['confirm_totp_token'] = "Bevestig de wijzigingen door de, door je authenticatie-app gegenereerde code, in te voeren.";
 
 $lang['admin']['rspamd-com_settings'] = '<a href="https://rspamd.com/doc/configuration/settings.html#settings-structure" target="_blank">Rspamd documentatie</a> - Een beschrijving voor deze instelling zal automatisch worden gegenereerd, bekijk de onderstaande presets voor meer info.';
 
@@ -636,13 +657,15 @@ $lang['admin']['queue_unban'] = "markeer om toe te staan";
 $lang['admin']['no_active_bans'] = "Geen actieve verbanningen";
 
 $lang['admin']['quarantine'] = "Quarantaine";
-$lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak<br />Gebruik 0 om deze functionaliteit <b>uit te zetten</b>.";
-$lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd)<br />0 betekent <b>niet</b> onbeperkt!";
-$lang['admin']['quarantine_exclude_domains'] = "Sluit domeinen en aliasdomeinen uit";
-$lang['admin']['quarantine_release_format'] = "Vrijgegeven items worden verstuurd als";
+$lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak:<br><small>Gebruik 0 om deze functionaliteit <b>uit te zetten</b>.</small>";
+$lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd):<br><small>0 betekent <b>niet</b> onbeperkt!</small>";
+$lang['admin']['quarantine_exclude_domains'] = "Sluit de volgende domeinen en aliasdomeinen uit";
+$lang['admin']['quarantine_release_format'] = "Verstuur vrijgegeven items als";
 $lang['admin']['quarantine_release_format_raw'] = "Origineel";
 $lang['admin']['quarantine_release_format_att'] = "Bijlage";
-
+$lang['admin']['quarantine_notification_sender'] = "Afzender van meldingen";
+$lang['admin']['quarantine_notification_subject'] = "Onderwerp van meldingen";
+$lang['admin']['quarantine_notification_html'] = "Meldingsjabloon:<br><small>Laat leeg om de standaardsjabloon te herstellen.</small>";
 $lang['admin']['ui_texts'] = "UI-labels en teksten";
 $lang['admin']['help_text'] = "Pas hulpteksten onder inlogvenster aan (HTML toegestaan)";
 $lang['admin']['title_name'] = '"Mailcow UI" website-titel';
@@ -669,6 +692,7 @@ $lang['user']['spam_score_reset'] = "Herstel naar standaardwaarde";
 $lang['edit']['spam_policy'] = "Voeg onderdelen toe, of verwijder onderdelen van de witte en zwarte lijst";
 $lang['edit']['spam_alias'] = "Maak een nieuw tijdelijk alias aan, of pas deze aan";
 
+$lang['danger']['comment_too_long'] = "Opmerkingen mogen niet langer dan 160 karakters zijn";
 $lang['danger']['img_tmp_missing'] = "Kan afbeelding niet valideren, tijdelijk bestand niet gevonden";
 $lang['danger']['img_invalid'] = "Kan afbeelding niet valideren";
 $lang['danger']['invalid_mime_type'] = "Ongeldig mime-type";
@@ -699,6 +723,11 @@ $lang['quarantine']['subj'] = "Onderwerp";
 $lang['quarantine']['text_plain_content'] = "Inhoud (tekst)";
 $lang['quarantine']['text_from_html_content'] = "Inhoud (geconverteerde html)";
 $lang['quarantine']['atts'] = "Bijlagen";
+$lang['quarantine']['low_danger'] = "Laag risico";
+$lang['quarantine']['neutral_danger'] = "Neutraal/geen beoordeling";
+$lang['quarantine']['medium_danger'] = "Middelmatig risico";
+$lang['quarantine']['high_danger'] = "Hoog risico";
+$lang['quarantine']['danger'] = "Risico";
 $lang['warning']['fuzzy_learn_error'] = "Fuzzy hash training-fout: %s";
 $lang['danger']['spam_learn_error'] = "Spamtraining-fout: %s";
 $lang['success']['qlearn_spam'] = "Bericht %s werd als spam gemarkeerd en is verwijderd";
@@ -715,7 +744,7 @@ $lang['debug']['external_logs'] = 'Externe logs';
 $lang['debug']['static_logs'] = 'Statische logs';
 $lang['debug']['solr_uptime'] = 'Uptime';
 $lang['debug']['solr_started_at'] = 'Opgestart op';
-$lang['debug']['solr_last_modified'] = 'Laatst bewerkt op';
+$lang['debug']['solr_last_modified'] = 'Voor het laatst bijgewerkt op';
 $lang['debug']['solr_size'] = 'Grootte';
 $lang['debug']['solr_docs'] = 'Documenten';