andryyy 2019-02-04 22:36:17 +01:00
commit 9f2a6f13a5
No known key found for this signature in database
GPG Key ID: 8EC34FF2794E25EF
3 changed files with 234 additions and 165 deletions

View File

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

View File

@ -17,7 +17,7 @@ log_f() {
redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${1}")\"}" > /dev/null redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${1}")\"}" > /dev/null
else else
redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ 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 fi
} }
@ -36,7 +36,12 @@ log_f "OK" no_date
ACME_BASE=/var/lib/acme ACME_BASE=/var/lib/acme
SSL_EXAMPLE=/var/lib/ssl-example 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(){ reload_configurations(){
# Reading container IDs # Reading container IDs
@ -112,6 +117,19 @@ get_ipv6(){
echo ${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 [[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem
if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; then if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; 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..." log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..."
sleep 3650d sleep 3650d
exec $(readlink -f "$0") 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 fi
else else
if [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${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/fullchain.pem ${ACME_BASE}/acme/private/privkey.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..." log_f "Restoring previous acme certificate and restarting script..."
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem
cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem
# Restarting with env var set to trigger a restart, # Restarting with env var set to trigger a restart,
exec env TRIGGER_RESTART=1 $(readlink -f "$0") exec env TRIGGER_RESTART=1 $(readlink -f "$0")
fi 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 while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
sleep 2 sleep 2
done done
# 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... " log_f "Initializing, please wait... "
while true; do 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 if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
SKIP_IP_CHECK=y SKIP_IP_CHECK=y
fi fi
# Cleaning up and init validation arrays
unset SQL_DOMAIN_ARR unset SQL_DOMAIN_ARR
unset VALIDATED_CONFIG_DOMAINS unset VALIDATED_CONFIG_DOMAINS
unset ADDITIONAL_VALIDATED_SAN 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 SQL_DOMAIN_ARR
declare -a VALIDATED_CONFIG_DOMAINS declare -a VALIDATED_CONFIG_DOMAINS
declare -a ADDITIONAL_VALIDATED_SAN declare -a ADDITIONAL_VALIDATED_SAN
declare -a ADDITIONAL_WC_ARR
declare -a ADDITIONAL_SAN_ARR
IFS=',' read -r -a TMP_ARR <<< "${ADDITIONAL_SAN}" 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 for i in "${TMP_ARR[@]}" ; do
if [[ "$i" =~ \.\*$ ]]; then if [[ "$i" =~ \.\*$ ]]; then
ADDITIONAL_WC_ARR+=(${i::-2}) ADDITIONAL_WC_ARR+=(${i::-2})
@ -177,6 +223,8 @@ while true; do
done done
ADDITIONAL_WC_ARR+=('autodiscover') ADDITIONAL_WC_ARR+=('autodiscover')
# Start IP detection
log_f "Detecting IP addresses... " no_nl
IPV4=$(get_ipv4) IPV4=$(get_ipv4)
IPV6=$(get_ipv6) IPV6=$(get_ipv6)
log_f "OK" no_date log_f "OK" no_date
@ -194,23 +242,15 @@ while true; do
fi fi
fi fi
log_f "Waiting for domain table... " no_nl #########################################
while [[ -z ${DOMAIN_TABLE} ]]; do # IP and webroot challenge verification #
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
while read domains; do while read domains; do
SQL_DOMAIN_ARR+=("${domains}") SQL_DOMAIN_ARR+=("${domains}")
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs) done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs)
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" == ${MAILCOW_HOSTNAME} ]]; then if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
log_f "Skipping mailcow hostname (${MAILCOW_HOSTNAME}), will be added anyway"
else
A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1) A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1) AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1)
# Check if CNAME without v6 enabled target # Check if CNAME without v6 enabled target
@ -220,16 +260,24 @@ while true; do
if [[ ! -z ${AAAA_SUBDOMAIN} ]]; then if [[ ! -z ${AAAA_SUBDOMAIN} ]]; then
log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check" log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check"
if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
log_f "Confirmed AAAA record ${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}") VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
else
log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}, but HTTP validation failed"
fi
else else
log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))" log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))"
fi fi
elif [[ ! -z ${A_SUBDOMAIN} ]]; then elif [[ ! -z ${A_SUBDOMAIN} ]]; then
log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}" log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}"
if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
log_f "Confirmed A record ${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}") VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
else
log_f "Confirmed AAAA record ${A_SUBDOMAIN}, but HTTP validation failed"
fi
else else
log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})" log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})"
fi fi
@ -249,16 +297,24 @@ while true; do
if [[ ! -z ${AAAA_MAILCOW_HOSTNAME} ]]; then if [[ ! -z ${AAAA_MAILCOW_HOSTNAME} ]]; then
log_f "Found AAAA record for ${MAILCOW_HOSTNAME}: ${AAAA_MAILCOW_HOSTNAME} - skipping A record check" 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 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}" if verify_challenge_path "${MAILCOW_HOSTNAME}" 6; then
log_f "Confirmed AAAA record ${AAAA_MAILCOW_HOSTNAME}"
VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
else
log_f "Confirmed AAAA record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
fi
else else
log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} ($(expand ${AAAA_MAILCOW_HOSTNAME}))" log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} ($(expand ${AAAA_MAILCOW_HOSTNAME}))"
fi fi
elif [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then elif [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}" log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
if verify_challenge_path "${MAILCOW_HOSTNAME}" 4; then
log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}" log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}"
VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
else
log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed"
fi
else else
log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME})" log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME})"
fi fi
@ -290,16 +346,24 @@ while true; do
if [[ ! -z ${AAAA_SAN} ]]; then if [[ ! -z ${AAAA_SAN} ]]; then
log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check" log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check"
if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
log_f "Confirmed AAAA record ${SAN}" if verify_challenge_path "${SAN}" 6; then
log_f "Confirmed AAAA record ${AAAA_SAN}"
ADDITIONAL_VALIDATED_SAN+=("${SAN}") ADDITIONAL_VALIDATED_SAN+=("${SAN}")
else
log_f "Confirmed AAAA record ${AAAA_SAN}, but HTTP validation failed"
fi
else else
log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} ($(expand ${AAAA_SAN}))" log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} ($(expand ${AAAA_SAN}))"
fi fi
elif [[ ! -z ${A_SAN} ]]; then elif [[ ! -z ${A_SAN} ]]; then
log_f "Found A record for ${SAN}: ${A_SAN}" log_f "Found A record for ${SAN}: ${A_SAN}"
if [[ ${IPV4:-ERR} == ${A_SAN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if [[ ${IPV4:-ERR} == ${A_SAN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then
if verify_challenge_path "${SAN}" 4; then
log_f "Confirmed A record ${A_SAN}" log_f "Confirmed A record ${A_SAN}"
ADDITIONAL_VALIDATED_SAN+=("${SAN}") ADDITIONAL_VALIDATED_SAN+=("${SAN}")
else
log_f "Confirmed A record ${A_SAN}, but HTTP validation failed"
fi
else else
log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (${A_SAN})" log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (${A_SAN})"
fi fi
@ -317,123 +381,97 @@ while true; do
exec $(readlink -f "$0") exec $(readlink -f "$0")
fi fi
array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED # Collecting SANs from active certificate
if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then SAN_NAMES=$(openssl x509 -noout -text -in ${ACME_BASE}/cert.pem | awk '/X509v3 Subject Alternative Name/ {getline;gsub(/ /, "", $0); print}' | tr -d "DNS:")
DATE=$(date +%Y-%m-%d_%H_%M_%S) if [[ ! -z ${SAN_NAMES} ]]; then
log_f "Found orphaned SAN ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/, keeping key file..." IFS=',' read -a SAN_ARRAY_NOW <<< ${SAN_NAMES}
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 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[*]} ]]; 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 if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
log_f "Using Let's Encrypt staging servers" log_f "Using Let's Encrypt staging servers"
STAGING_PARAMETER="-s" STAGING_PARAMETER='--directory-url https://acme-staging-v02.api.letsencrypt.org/directory'
else else
STAGING_PARAMETER= STAGING_PARAMETER=
fi fi
ACME_RESPONSE=$(acme-client \ # acme-tiny writes info to stderr and ceritifcate to stdout
-v -e -b -N -n ${STAGING_PARAMETER} \ # The redirects will do the following:
-a 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf' \ # - redirect stdout to temp certificate file
-f ${ACME_BASE}/acme/private/account.key \ # - redirect acme-tiny stderr to stdout (logs to variable ACME_RESPONSE)
-k ${ACME_BASE}/acme/private/privkey.pem \ # - tee stderr to get live output and log to dockerd
-c ${ACME_BASE}/acme \
${ALL_VALIDATED[*]} 2>&1 | tee /dev/fd/5)
case "$?" in
0) # new certs
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 ACME_RESPONSE=$(acme-tiny ${STAGING_PARAMETER} \
if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then --account-key ${ACME_BASE}/acme/account.pem \
log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..." --disable-check \
cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem --csr ${ACME_BASE}/acme/acme.csr \
cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5)
fi
reload_configurations case "$?" in
;; 0) # cert requested
1) # failure ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" redis_only b64 log_f "${ACME_RESPONSE_B64}" redis_only b64
if [[ $ACME_RESPONSE =~ "No registration exists" ]]; then log_f "Deploying..."
log_f "Registration keys are invalid, deleting old keys and restarting..." # Deploy the new certificate and key
rm ${ACME_BASE}/acme/private/account.key # Moving temp cert to acme/cert.pem
exec $(readlink -f "$0") if verify_hash_match /tmp/_cert.pem ${ACME_BASE}/acme/key.pem; then
fi mv /tmp/_cert.pem ${ACME_BASE}/acme/cert.pem
if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem
log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...." cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem
cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem reload_configurations
cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem rm /var/www/acme/*
TRIGGER_RESTART=1 log_f "Certificate successfully deployed, removing backup, sleeping 1d"
elif [[ -f ${ACME_BASE}/acme/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/privkey.pem ]]; then sleep 1d
log_f "Error requesting certificate, restoring from previous acme request and restarting containers..." else
cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, ignoring certificate"
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..." log_f "Retrying in 30 minutes..."
sleep 30m sleep 30m
exec $(readlink -f "$0") exec $(readlink -f "$0")
fi
;; ;;
2) # no change *) # non-zero is non-fun
ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" redis_only b64 log_f "${ACME_RESPONSE_B64}" redis_only b64
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)
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..." log_f "Retrying in 30 minutes..."
sleep 30m sleep 30m
exec $(readlink -f "$0") exec $(readlink -f "$0")
;; ;;
esac esac
log_f "ACME certificate validation done. Sleeping for another day."
sleep 1d
done done

View File

@ -46,7 +46,7 @@ $lang['danger']['malformed_username'] = "Ongeldige gebruikersnaam";
$lang['info']['awaiting_tfa_confirmation'] = "In afwachting van tweefactorauthenticatie..."; $lang['info']['awaiting_tfa_confirmation'] = "In afwachting van tweefactorauthenticatie...";
$lang['success']['logged_in_as'] = "Succesvol ingelogd als %s"; $lang['success']['logged_in_as'] = "Succesvol ingelogd als %s";
$lang['danger']['login_failed'] = "Aanmelding mislukt"; $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']['no_user_defined'] = "Geen gebruiker gespecificeerd";
$lang['danger']['script_empty'] = "Script dient ingevuld te worden"; $lang['danger']['script_empty'] = "Script dient ingevuld te worden";
$lang['danger']['sieve_error'] = "Sieve-fout: %s"; $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['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['danger']['rl_timeframe'] = "Ratelimit-tijdsbestek is ongeldig";
$lang['success']['rl_saved'] = "Ratelimit voor object %s is opgeslagen"; $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_syncjobs'] = "Synchronisatietaken %s zijn verwijderd";
$lang['success']['deleted_syncjob'] = "Synchronisatietaak %s is verwijderd"; $lang['success']['deleted_syncjob'] = "Synchronisatietaak %s is verwijderd";
$lang['success']['delete_filters'] = "Filters %s zijn 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']['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']['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']['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']['domain_not_empty'] = "Kan geen domein in gebruik verwijderen";
$lang['danger']['validity_missing'] = 'Wijs een geldigheidstermijn toe'; $lang['danger']['validity_missing'] = 'Wijs een geldigheidstermijn toe';
$lang['user']['loading'] = "Bezig met laden..."; $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_subfolder'] = 'In submap';
$lang['user']['tag_in_subject'] = 'In onderwerp'; $lang['user']['tag_in_subject'] = 'In onderwerp';
$lang['user']['tag_in_none'] = 'Niets doen'; $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_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>+Apple</b>@example.org'; $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'] = 'Herstel ActiveSync-apparaatcache';
$lang['user']['eas_reset_now'] = 'Herstel nu'; $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'] = 'Herstel SOGo-profiel';
$lang['user']['sogo_profile_reset_now'] = 'Herstel nu'; $lang['user']['sogo_profile_reset_now'] = 'Herstel nu';
@ -251,9 +251,9 @@ $lang['user']['edit'] = 'Wijzig';
$lang['user']['remove'] = 'Verwijder'; $lang['user']['remove'] = 'Verwijder';
$lang['user']['create_syncjob'] = 'Voeg een nieuwe synchronisatietaak toe'; $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']['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['start']['help'] = 'Toon/verberg hulppaneel';
$lang['header']['mailcow_settings'] = 'Instellingen'; $lang['header']['mailcow_settings'] = 'Instellingen';
$lang['header']['administration'] = 'Configuratie & details'; $lang['header']['administration'] = 'Configuratie & details';
@ -384,7 +384,14 @@ $lang['edit']['multiple_bookings'] = 'Meerdere boekingen';
$lang['edit']['kind'] = 'Soort'; $lang['edit']['kind'] = 'Soort';
$lang['edit']['resource'] = 'Hulpbron'; $lang['edit']['resource'] = 'Hulpbron';
$lang['edit']['relayhost'] = 'Afzender-afhankelijke transportkaarten'; $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']['spam_alias'] = 'Tijdelijke aliassen';
$lang['acl']['tls_policy'] = 'Versleutelingsbeleid'; $lang['acl']['tls_policy'] = 'Versleutelingsbeleid';
$lang['acl']['spam_score'] = 'Spamscore'; $lang['acl']['spam_score'] = 'Spamscore';
@ -393,14 +400,28 @@ $lang['acl']['delimiter_action'] = 'Delimiter-actie';
$lang['acl']['syncjobs'] = 'Synchronisatietaken'; $lang['acl']['syncjobs'] = 'Synchronisatietaken';
$lang['acl']['eas_reset'] = 'Herstel ActiveSync-apparaatcache'; $lang['acl']['eas_reset'] = 'Herstel ActiveSync-apparaatcache';
$lang['acl']['sogo_profile_reset'] = 'Herstel SOGo-profiel'; $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']['alias_domains'] = 'Voeg aliasdomeinen toe';
$lang['acl']['login_as'] = 'Log in als postvakgebruiker'; $lang['acl']['login_as'] = 'Log in als postvakgebruiker';
$lang['acl']['bcc_maps'] = 'BCC-kaarten'; $lang['acl']['bcc_maps'] = 'BCC-kaarten';
$lang['acl']['filters'] = 'Filters'; $lang['acl']['filters'] = 'Filters';
$lang['acl']['ratelimit'] = 'Ratelimit'; $lang['acl']['ratelimit'] = 'Ratelimit';
$lang['acl']['recipient_maps'] = 'Ontvanger-kaarten'; $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']['generate'] = 'genereer';
$lang['add']['syncjob'] = 'Voeg een nieuwe synchronisatietaak toe'; $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']['confirm'] = "Bevestig";
$lang['tfa']['totp'] = "TOTP (Google Authenticator etc.)"; $lang['tfa']['totp'] = "TOTP (Google Authenticator etc.)";
$lang['tfa']['select'] = "Selecteer..."; $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_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 uw wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken."; $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 uw authenticatie-app:"; $lang['tfa']['scan_qr_code'] = "Scan de volgende QR-code met je authenticatie-app:";
$lang['tfa']['enter_qr_code'] = "Voer deze code in als uw apparaat geen QR-codes kan scannen:"; $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 uw authenticatie-app gegenereerde code, in te voeren."; $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.'; $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']['no_active_bans'] = "Geen actieve verbanningen";
$lang['admin']['quarantine'] = "Quarantaine"; $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_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 />0 betekent <b>niet</b> onbeperkt!"; $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 domeinen en aliasdomeinen uit"; $lang['admin']['quarantine_exclude_domains'] = "Sluit de volgende domeinen en aliasdomeinen uit";
$lang['admin']['quarantine_release_format'] = "Vrijgegeven items worden verstuurd als"; $lang['admin']['quarantine_release_format'] = "Verstuur vrijgegeven items als";
$lang['admin']['quarantine_release_format_raw'] = "Origineel"; $lang['admin']['quarantine_release_format_raw'] = "Origineel";
$lang['admin']['quarantine_release_format_att'] = "Bijlage"; $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']['ui_texts'] = "UI-labels en teksten";
$lang['admin']['help_text'] = "Pas hulpteksten onder inlogvenster aan (HTML toegestaan)"; $lang['admin']['help_text'] = "Pas hulpteksten onder inlogvenster aan (HTML toegestaan)";
$lang['admin']['title_name'] = '"Mailcow UI" website-titel'; $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_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['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_tmp_missing'] = "Kan afbeelding niet valideren, tijdelijk bestand niet gevonden";
$lang['danger']['img_invalid'] = "Kan afbeelding niet valideren"; $lang['danger']['img_invalid'] = "Kan afbeelding niet valideren";
$lang['danger']['invalid_mime_type'] = "Ongeldig mime-type"; $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_plain_content'] = "Inhoud (tekst)";
$lang['quarantine']['text_from_html_content'] = "Inhoud (geconverteerde html)"; $lang['quarantine']['text_from_html_content'] = "Inhoud (geconverteerde html)";
$lang['quarantine']['atts'] = "Bijlagen"; $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['warning']['fuzzy_learn_error'] = "Fuzzy hash training-fout: %s";
$lang['danger']['spam_learn_error'] = "Spamtraining-fout: %s"; $lang['danger']['spam_learn_error'] = "Spamtraining-fout: %s";
$lang['success']['qlearn_spam'] = "Bericht %s werd als spam gemarkeerd en is verwijderd"; $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']['static_logs'] = 'Statische logs';
$lang['debug']['solr_uptime'] = 'Uptime'; $lang['debug']['solr_uptime'] = 'Uptime';
$lang['debug']['solr_started_at'] = 'Opgestart op'; $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_size'] = 'Grootte';
$lang['debug']['solr_docs'] = 'Documenten'; $lang['debug']['solr_docs'] = 'Documenten';