Browse Source

Merge, conflict fixed

andryyy 8 years ago
parent
commit
cf902854d7

+ 1 - 0
data/Dockerfiles/acme/Dockerfile

@@ -8,6 +8,7 @@ RUN apk add --update --no-cache \
 	curl \
 	curl \
 	openssl \
 	openssl \
 	bind-tools \
 	bind-tools \
+	jq \
 	mariadb-client
 	mariadb-client
 
 
 COPY docker-entrypoint.sh /srv/docker-entrypoint.sh
 COPY docker-entrypoint.sh /srv/docker-entrypoint.sh

+ 20 - 16
data/Dockerfiles/acme/docker-entrypoint.sh

@@ -2,10 +2,12 @@
 
 
 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/private
 
 
 restart_containers(){
 restart_containers(){
 	for container in $*; do
 	for container in $*; do
+		echo "Restarting ${container}..."
 		curl -X POST \
 		curl -X POST \
 			--unix-socket /var/run/docker.sock \
 			--unix-socket /var/run/docker.sock \
 			"http/containers/${container}/restart"
 			"http/containers/${container}/restart"
@@ -45,14 +47,14 @@ else
 			echo "Restoring previous acme certificate and restarting script..."
 			echo "Restoring previous acme certificate and restarting script..."
 			cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
 			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/private/privkey.pem ${ACME_BASE}/key.pem
-			exec $(readlink -f "$0")
+			exec env TRIGGER_RESTART=1 $(readlink -f "$0")
 		fi
 		fi
 	ISSUER="mailcow"
 	ISSUER="mailcow"
 	else
 	else
 		echo "Restoring mailcow snake-oil certificates and restarting script..."
 		echo "Restoring mailcow snake-oil certificates and restarting script..."
 		cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem
 		cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem
 		cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 		cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
-		exec $(readlink -f "$0")
+		exec env TRIGGER_RESTART=1 $(readlink -f "$0")
 	fi
 	fi
 fi
 fi
 
 
@@ -66,6 +68,8 @@ while true; do
 	declare -a ADDITIONAL_VALIDATED_SAN
 	declare -a ADDITIONAL_VALIDATED_SAN
 	IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}"
 	IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}"
 	IPV4=$(curl -4s https://mailcow.email/ip.php)
 	IPV4=$(curl -4s https://mailcow.email/ip.php)
+	# 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" " "))
 
 
 	while read line; do
 	while read line; do
 		SQL_DOMAIN_ARR+=("${line}")
 		SQL_DOMAIN_ARR+=("${line}")
@@ -75,7 +79,7 @@ while true; do
 		A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1)
 		A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1)
 		if [[ ! -z ${A_CONFIG} ]]; then
 		if [[ ! -z ${A_CONFIG} ]]; then
 			echo "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}"
 			echo "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}"
-			if [[ ${IPV4} == ${A_CONFIG} ]]; then
+			if [[ ${IPV4:-ERR} == ${A_CONFIG} ]]; then
 				echo "Confirmed A record autoconfig.${SQL_DOMAIN}"
 				echo "Confirmed A record autoconfig.${SQL_DOMAIN}"
 				VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}")
 				VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}")
 			else
 			else
@@ -88,7 +92,7 @@ while true; do
         A_DISCOVER=$(dig A autodiscover.${SQL_DOMAIN} +short | tail -n 1)
         A_DISCOVER=$(dig A autodiscover.${SQL_DOMAIN} +short | tail -n 1)
 		if [[ ! -z ${A_DISCOVER} ]]; then
 		if [[ ! -z ${A_DISCOVER} ]]; then
 			echo "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}"
 			echo "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}"
-			if [[ ${IPV4} == ${A_DISCOVER} ]]; then
+			if [[ ${IPV4:-ERR} == ${A_DISCOVER} ]]; then
 				echo "Confirmed A record autodiscover.${SQL_DOMAIN}"
 				echo "Confirmed A record autodiscover.${SQL_DOMAIN}"
 				VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}")
 				VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}")
 			else
 			else
@@ -102,7 +106,7 @@ while true; do
 	A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1)
 	A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1)
 	if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
 	if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
 		echo "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
 		echo "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
-		if [[ ${IPV4} == ${A_MAILCOW_HOSTNAME} ]]; then
+		if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]]; then
 			echo "Confirmed A record ${MAILCOW_HOSTNAME}"
 			echo "Confirmed A record ${MAILCOW_HOSTNAME}"
 			VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
 			VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
 		else
 		else
@@ -116,7 +120,7 @@ while true; do
 		A_SAN=$(dig A ${SAN} +short | tail -n 1)
 		A_SAN=$(dig A ${SAN} +short | tail -n 1)
 		if [[ ! -z ${A_SAN} ]]; then
 		if [[ ! -z ${A_SAN} ]]; then
 			echo "Found A record for ${SAN}: ${A_SAN}"
 			echo "Found A record for ${SAN}: ${A_SAN}"
-			if [[ ${IPV4} == ${A_SAN} ]]; then
+			if [[ ${IPV4:-ERR} == ${A_SAN} ]]; then
 				echo "Confirmed A record ${SAN}"
 				echo "Confirmed A record ${SAN}"
 				ADDITIONAL_VALIDATED_SAN+=("${SAN}")
 				ADDITIONAL_VALIDATED_SAN+=("${SAN}")
 			else
 			else
@@ -127,7 +131,7 @@ while true; do
 		fi
 		fi
 	done
 	done
 
 
-	ALL_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${VALIDATED_MAILCOW_HOSTNAME}))
+	ALL_VALIDATED="$(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${VALIDATED_MAILCOW_HOSTNAME})"
 	if [[ -z ${ALL_VALIDATED[*]} ]]; then
 	if [[ -z ${ALL_VALIDATED[*]} ]]; then
 		echo "Cannot validate hostnames, skipping Let's Encrypt..."
 		echo "Cannot validate hostnames, skipping Let's Encrypt..."
 		echo 0
 		echo 0
@@ -136,7 +140,7 @@ while true; do
 	ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${MAILCOW_HOSTNAME} | tr ' ' '\n' | sort | uniq -u ))
 	ORPHANED_SAN=($(echo ${SAN_ARRAY_NOW[*]} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${MAILCOW_HOSTNAME} | tr ' ' '\n' | sort | uniq -u ))
 	if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then
 	if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then
 		DATE=$(date +%Y-%m-%d_%H_%M_%S)
 		DATE=$(date +%Y-%m-%d_%H_%M_%S)
-		echo "Found orphaned SAN(s) ${ORPHANED_SAN[*]} in certificate, moving old files to ${ACME_BASE}/acme/private/${DATE}.bak/"
+		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/
 		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/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/private/${DATE}.bak/
 		mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/
 		mv ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/acme/private/${DATE}.bak/
@@ -159,11 +163,11 @@ while true; do
 
 
 			# restart docker containers
 			# restart docker containers
 			if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then
 			if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then
-				echo "Certificate was successfully request, but key and certificate have non-matching hashes, restoring mailcow snake-oil and restarting containers..."
+				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}/cert.pem ${ACME_BASE}/cert.pem
 				cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 				cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 			fi
 			fi
-			restart_containers ${CONTAINERS_RESTART}
+			restart_containers ${CONTAINERS_RESTART[*]}
 			;;
 			;;
 		1) # failure
 		1) # failure
 			if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
 			if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
@@ -171,7 +175,7 @@ while true; do
 				cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem
 				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
 				cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem
 				TRIGGER_RESTART=1
 				TRIGGER_RESTART=1
-			elif [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
+            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..."
 				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/fullchain.pem ${ACME_BASE}/cert.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
@@ -183,20 +187,20 @@ while true; do
 				cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 				cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 				TRIGGER_RESTART=1
 				TRIGGER_RESTART=1
 			fi
 			fi
-			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART}
+			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
 			exit 1;;
 			exit 1;;
 		2) # no change
 		2) # no change
 			if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then
 			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..."
 				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/fullchain.pem ${ACME_BASE}/cert.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
-				restart_containers ${CONTAINERS_RESTART}
+				restart_containers ${CONTAINERS_RESTART[*]}
 			fi
 			fi
 			if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then
 			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..."
 				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/fullchain.pem ${ACME_BASE}/cert.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
-				restart_containers ${CONTAINERS_RESTART}
+				restart_containers ${CONTAINERS_RESTART[*]}
 			fi
 			fi
 			;;
 			;;
 		*) # unspecified
 		*) # unspecified
@@ -205,7 +209,7 @@ while true; do
 				cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem
 				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
 				cp ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ${ACME_BASE}/key.pem
 				TRIGGER_RESTART=1
 				TRIGGER_RESTART=1
-            elif [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
+            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..."
 				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/fullchain.pem ${ACME_BASE}/cert.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
@@ -217,7 +221,7 @@ while true; do
 				cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 				cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
 				TRIGGER_RESTART=1
 				TRIGGER_RESTART=1
 			fi
 			fi
-			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART}
+			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
 			exit 1;;
 			exit 1;;
 	esac
 	esac
 
 

+ 2 - 2
data/Dockerfiles/dovecot/Dockerfile

@@ -3,8 +3,8 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 
 ARG DEBIAN_FRONTEND=noninteractive
 ARG DEBIAN_FRONTEND=noninteractive
 ENV LC_ALL C
 ENV LC_ALL C
-ENV DOVECOT_VERSION 2.2.30.2
-ENV PIGEONHOLE_VERSION 0.4.18
+ENV DOVECOT_VERSION 2.2.31
+ENV PIGEONHOLE_VERSION 0.4.19
 
 
 RUN apt-get update && apt-get -y install \
 RUN apt-get update && apt-get -y install \
 	automake \
 	automake \

+ 37 - 11
data/Dockerfiles/fail2ban/logwatch.py

@@ -19,12 +19,33 @@ if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
 	raise SystemExit
 	raise SystemExit
 
 
 r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
 r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
-RULES = {
-	'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
-	'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
-	'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked',
-	'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)',
-}
+client = docker.from_env()
+
+for container in client.containers.list():
+	if "postfix-mailcow" in container.name:
+		postfix_container = container.name
+	elif "dovecot-mailcow" in container.name:
+		dovecot_container = container.name
+	elif "sogo-mailcow" in container.name:
+		sogo_container = container.name
+	elif "php-fpm-mailcow" in container.name:
+		php_fpm_container = container.name
+
+RULES = {}
+
+RULES[postfix_container] = {}
+RULES[dovecot_container] = {}
+RULES[sogo_container] = {}
+RULES[php_fpm_container] = {}
+
+RULES[postfix_container][1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed'
+RULES[dovecot_container][1] = '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),'
+RULES[dovecot_container][2] = '-login: Disconnected \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+RULES[dovecot_container][3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+RULES[dovecot_container][4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+RULES[sogo_container][1] = 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked'
+RULES[php_fpm_container][1] = 'mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)'
+
 
 
 r.setnx("F2B_BAN_TIME", "1800")
 r.setnx("F2B_BAN_TIME", "1800")
 r.setnx("F2B_MAX_ATTEMPTS", "10")
 r.setnx("F2B_MAX_ATTEMPTS", "10")
@@ -135,12 +156,17 @@ def watch(container):
 	log['message'] = "Watching %s" % container
 	log['message'] = "Watching %s" % container
 	r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
 	r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
 	print "Watching", container
 	print "Watching", container
-	client = docker.from_env()
 	for msg in client.containers.get(container).attach(stream=True, logs=False):
 	for msg in client.containers.get(container).attach(stream=True, logs=False):
-		result = re.search(RULES[container], msg)
-		if result:
-			addr = result.group(1)
-			ban(addr)
+		for rule_id, rule_regex in RULES[container].iteritems():
+			result = re.search(rule_regex, msg)
+			if result:
+				addr = result.group(1)
+				print "%s matched rule id %d in %s" % (addr, rule_id, container)
+				log['time'] = int(round(time.time()))
+				log['priority'] = "warn"
+				log['message'] = "%s matched rule id %d in %s" % (addr, rule_id, container)
+				r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+				ban(addr)
 
 
 def autopurge():
 def autopurge():
 	while not quit_now:
 	while not quit_now:

+ 93 - 176
data/conf/rspamd/dynmaps/settings.php

@@ -4,15 +4,10 @@ The match section performs AND operation on different matches: for example, if y
 then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
 then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
 then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
 then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
 */
 */
-function parse_email($email) {
-  if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
-  $a = strrpos($email, '@');
-  return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
-}
 header('Content-Type: text/plain');
 header('Content-Type: text/plain');
 require_once "vars.inc.php";
 require_once "vars.inc.php";
 
 
-ini_set('error_reporting', 0);
+ini_set('error_reporting', 1);
 
 
 $dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
 $dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
 $opt = [
 $opt = [
@@ -29,6 +24,77 @@ catch (PDOException $e) {
   exit;
   exit;
 }
 }
 
 
+function parse_email($email) {
+  if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
+  $a = strrpos($email, '@');
+  return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
+}
+
+function ucl_rcpts($object, $type) {
+  global $pdo;
+  if ($type == 'mailbox') {
+    // Standard aliases
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias`
+      WHERE `goto` LIKE :object_goto
+        AND `address` NOT LIKE '@%'
+        AND `address` != :object_address");
+    $stmt->execute(array(
+      ':object_goto' => '%' . $object . '%',
+      ':object_address' => $object
+    ));
+    $standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($standard_aliases)) {
+      $local = parse_email($row['address'])['local'];
+      $domain = parse_email($row['address'])['domain'];
+      if (!empty($local) && !empty($domain)) {
+        $rcpt[] = '/' . $local . '\+.*' . $domain . '/';
+      }
+      $rcpt[] = $row['address'];
+    }
+    // Aliases by alias domains
+    $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` 
+      LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
+      WHERE `mailbox`.`username` = :object");
+    $stmt->execute(array(
+      ':object' => $object
+    ));
+    $by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    array_filter($by_domain_aliases);
+    while ($row = array_shift($by_domain_aliases)) {
+      if (!empty($row['alias'])) {
+        $local = parse_email($row['alias'])['local'];
+        $domain = parse_email($row['alias'])['domain'];
+        if (!empty($local) && !empty($domain)) {
+          $rcpt[] = '/' . $local . '\+.*' . $domain . '/';
+        }
+      $rcpt[] = $row['alias'];
+      }
+    }
+    // Mailbox self
+    $local = parse_email($row['object'])['local'];
+    $domain = parse_email($row['object'])['domain'];
+    if (!empty($local) && !empty($domain)) {
+      $rcpt[] = '/' . $local . '\+.*' . $domain . '/';
+    }
+    $rcpt[] = $object;
+  }
+  elseif ($type == 'domain') {
+    // Domain self
+		$rcpt[] = '/.*@' . $object . '/';
+		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
+			WHERE `target_domain` = :object");
+		$stmt->execute(array(':object' => $row['object']));
+		$alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
+		array_filter($alias_domains);
+		while ($row = array_shift($alias_domains)) {
+      $rcpt[] = '/.*@' . $row['alias_domain'] . '/';
+		}
+  }
+  if (!empty($rcpt)) {
+    return $rcpt;
+  }
+  return false;
+}
 ?>
 ?>
 settings {
 settings {
 <?php
 <?php
@@ -44,73 +110,18 @@ while ($row = array_shift($rows)) {
 	$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
 	$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
 ?>
 ?>
 	score_<?=$username_sane;?> {
 	score_<?=$username_sane;?> {
-		priority = low;
+		priority = 4;
+<?php
+  foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
+?>
+		rcpt = "<?=$rcpt;?>";
 <?php
 <?php
+  }
 	$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` 
 	$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` 
 		WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
 		WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
 			AND `object`= :object");
 			AND `object`= :object");
 	$stmt->execute(array(':object' => $row['object']));
 	$stmt->execute(array(':object' => $row['object']));
 	$spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
 	$spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
-
-	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
-		WHERE (`object`= :object OR `object`= :object_domain)
-			AND (`option` = 'blacklist_from' OR `option` = 'whitelist_from')");
-	$stmt->execute(array(':object' => $row['object'], ':object_domain' => substr(strrchr($row['object'], "@"), 1)));
-	$grouped_lists = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	array_filter($grouped_lists);
-    while ($grouped_list = array_shift($grouped_lists)) {
-        $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_list['value'])));
-        if (!empty($value_sane)) {
-?>
-		from = "/^((?!<?=$value_sane;?>).)*$/";
-<?php
-        }
-    }
-    $local = parse_email($row['object'])['local'];
-    $domain = parse_email($row['object'])['domain'];
-    if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-    }
-?>
-		rcpt = "<?=$row['object'];?>";
-<?php
-	$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address");
-	$stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object']));
-	$rows_aliases_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	while ($row_aliases_1 = array_shift($rows_aliases_1)) {
-    $local = parse_email($row_aliases_1['address'])['local'];
-    $domain = parse_email($row_aliases_1['address'])['domain'];
-    if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-    }
-?>
-		rcpt = "<?=$row_aliases_1['address'];?>";
-<?php
-	}
-	$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox` 
-		LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain`
-		WHERE `mailbox`.`username` = :object");
-	$stmt->execute(array(':object' => $row['object']));
-	$rows_aliases_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	array_filter($rows_aliases_2);
-	while ($row_aliases_2 = array_shift($rows_aliases_2)) {
-    if (!empty($row_aliases_2['aliases'])) {
-      $local = parse_email($row_aliases_2['aliases'])['local'];
-      $domain = parse_email($row_aliases_2['aliases'])['domain'];
-      if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-      }
-?>
-		rcpt = "<?=$row_aliases_2['aliases'];?>";
-<?php
-    }
-	}
 ?>
 ?>
 		apply "default" {
 		apply "default" {
 			actions {
 			actions {
@@ -145,70 +156,23 @@ while ($row = array_shift($rows)) {
 <?php
 <?php
 	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 ?>
 ?>
-		priority = medium;
-		rcpt = "/.*@<?=$row['object'];?>/";
+		priority = 5;
 <?php
 <?php
-		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
-			WHERE `target_domain` = :object");
-		$stmt->execute(array(':object' => $row['object']));
-		$rows_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
-		array_filter($rows_domain_aliases);
-		while ($row_domain_aliases = array_shift($rows_domain_aliases)) {
+		foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
 ?>
 ?>
-		rcpt = "/.*@<?=$row_domain_aliases['alias_domain'];?>/";
+		rcpt = "<?=$rcpt;?>";
 <?php
 <?php
 		}
 		}
 	}
 	}
 	else {
 	else {
 ?>
 ?>
-		priority = high;
+		priority = 6;
 <?php
 <?php
-    $local = parse_email($row['object'])['local'];
-    $domain = parse_email($row['object'])['domain'];
-    if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-    }
+		foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
 ?>
 ?>
-		rcpt = "<?=$row['object'];?>";
+		rcpt = "<?=$rcpt;?>";
 <?php
 <?php
-	}
-	$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address");
-	$stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object']));
-	$rows_aliases_wl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	array_filter($rows_aliases_wl_1);
-	while ($row_aliases_wl_1 = array_shift($rows_aliases_wl_1)) {
-      $local = parse_email($row_aliases_wl_1['address'])['local'];
-      $domain = parse_email($row_aliases_wl_1['address'])['domain'];
-      if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-      }
-?>
-		rcpt = "<?=$row_aliases_wl_1['address'];?>";
-<?php
-	}
-	$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox` 
-		LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain`
-		WHERE `mailbox`.`username` = :object");
-	$stmt->execute(array(':object' => $row['object']));
-	$rows_aliases_wl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	array_filter($rows_aliases_wl_2);
-	while ($row_aliases_wl_2 = array_shift($rows_aliases_wl_2)) {
-    if (!empty($row_aliases_wl_2['aliases'])) {
-      $local = parse_email($row_aliases_wl_2['aliases'])['local'];
-      $domain = parse_email($row_aliases_wl_2['aliases'])['domain'];
-      if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-      }
-?>
-		rcpt = "<?=$row_aliases_wl_2['aliases'];?>";
-<?php
-    }
+		}
 	}
 	}
 ?>
 ?>
 		apply "default" {
 		apply "default" {
@@ -243,70 +207,23 @@ while ($row = array_shift($rows)) {
 <?php
 <?php
 	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 ?>
 ?>
-		priority = medium;
-		rcpt = "/.*@<?=$row['object'];?>/";
+		priority = 5;
 <?php
 <?php
-		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
-			WHERE `target_domain` = :object");
-		$stmt->execute(array(':object' => $row['object']));
-		$rows_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
-		array_filter($rows_domain_aliases);
-		while ($row_domain_aliases = array_shift($rows_domain_aliases)) {
+		foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
 ?>
 ?>
-		rcpt = "/.*@<?=$row_domain_aliases['alias_domain'];?>/";
+		rcpt = "<?=$rcpt;?>";
 <?php
 <?php
 		}
 		}
 	}
 	}
 	else {
 	else {
 ?>
 ?>
-		priority = high;
+		priority = 6;
 <?php
 <?php
-    $local = parse_email($row['object'])['local'];
-    $domain = parse_email($row['object'])['domain'];
-    if (!empty($local) && !empty($local)) {
+		foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
 ?>
 ?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
+		rcpt = "<?=$rcpt;?>";
 <?php
 <?php
-    }
-?>
-		rcpt = "<?=$row['object'];?>";
-<?php
-	}
-	$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address");
-	$stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object']));
-	$rows_aliases_bl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	array_filter($rows_aliases_bl_1);
-	while ($row_aliases_bl_1 = array_shift($rows_aliases_bl_1)) {
-      $local = parse_email($row_aliases_bl_1['address'])['local'];
-      $domain = parse_email($row_aliases_bl_1['address'])['domain'];
-      if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-      }
-?>
-		rcpt = "<?=$row_aliases_bl_1['address'];?>";
-<?php
-	}
-	$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox` 
-		LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain`
-		WHERE `mailbox`.`username` = :object");
-	$stmt->execute(array(':object' => $row['object']));
-	$rows_aliases_bl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
-	array_filter($rows_aliases_bl_2);
-	while ($row_aliases_bl_2 = array_shift($rows_aliases_bl_2)) {
-    if (!empty($row_aliases_bl_2['aliases'])) {
-      $local = parse_email($row_aliases_bl_2['aliases'])['local'];
-      $domain = parse_email($row_aliases_bl_2['aliases'])['domain'];
-      if (!empty($local) && !empty($local)) {
-?>
-		rcpt = "/<?=$local;?>\+.*<?=$domain;?>/";
-<?php
-      }
-?>
-		rcpt = "<?=$row_aliases_bl_2['aliases'];?>";
-<?php
-    }
+		}
 	}
 	}
 ?>
 ?>
 		apply "default" {
 		apply "default" {
@@ -319,4 +236,4 @@ while ($row = array_shift($rows)) {
 <?php
 <?php
 }
 }
 ?>
 ?>
-}
+}

+ 27 - 6
data/web/admin.php

@@ -230,6 +230,27 @@ $tfa_data = get_tfa();
           </div>
           </div>
           <button class="btn btn-default" id="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
           <button class="btn btn-default" id="add_item" data-id="dkim" data-api-url='add/dkim' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
         </form>
         </form>
+
+        <legend data-target="#import_dkim" style="margin-top:40px;cursor:pointer" data-toggle="collapse">↳ <?=$lang['admin']['import_private_key'];?></legend>
+        <div id="import_dkim" class="collapse">
+        <form class="form" data-id="dkim_import" role="form" method="post">
+          <div class="form-group">
+            <label for="domain">Domain:</label>
+            <input class="form-control" id="domain" name="domain" placeholder="example.org" required>
+          </div>
+          <div class="form-group">
+            <label for="domain">Selector:</label>
+            <input class="form-control" id="dkim_selector" name="dkim_selector" value="dkim" required>
+          </div>
+          <div class="form-group">
+            <label for="private_key_file"><?=$lang['admin']['private_key'];?>:</label>
+            <textarea class="form-control" rows="5" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA PRIVATE KEY-----
+XYZ
+-----END RSA PRIVATE KEY-----"></textarea>
+          </div>
+          <button class="btn btn-default" id="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button>
+        </form>
+        </div>
       </div>
       </div>
     </div>
     </div>
     
     
@@ -271,26 +292,26 @@ $tfa_data = get_tfa();
     </div>
     </div>
 
 
     <div class="panel panel-default">
     <div class="panel panel-default">
-      <div class="panel-heading">Fail2Ban parameters</div>
+      <div class="panel-heading"><?=$lang['admin']['f2b_parameters'];?></div>
       <div class="panel-body">
       <div class="panel-body">
       <?php
       <?php
-      $f2b_data = get_f2b_parameters();
+      $f2b_data = fail2ban('get');
       ?>
       ?>
         <form class="form" data-id="f2b" role="form" method="post">
         <form class="form" data-id="f2b" role="form" method="post">
           <div class="form-group">
           <div class="form-group">
-            <label for="ban_time">Ban time (s):</label>
+            <label for="ban_time"><?=$lang['admin']['f2b_ban_time'];?>:</label>
             <input type="number" class="form-control" id="ban_time" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required>
             <input type="number" class="form-control" id="ban_time" name="ban_time" value="<?=$f2b_data['ban_time'];?>" required>
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
-            <label for="max_attempts">Max. attempts:</label>
+            <label for="max_attempts"><?=$lang['admin']['f2b_max_attempts'];?>:</label>
             <input type="number" class="form-control" id="max_attempts" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required>
             <input type="number" class="form-control" id="max_attempts" name="max_attempts" value="<?=$f2b_data['max_attempts'];?>" required>
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
-            <label for="retry_window">Retry window (s) for max. attempts:</label>
+            <label for="retry_window"><?=$lang['admin']['f2b_retry_window'];?>:</label>
             <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
             <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
           </div>
           </div>
           <div class="form-group">
           <div class="form-group">
-            <label for="retry_window">Whitelisted networks/hosts</label>
+            <label for="retry_window"><?=$lang['admin']['f2b_whitelist'];?>:</label>
             <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
             <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
           </div>
           </div>
           <button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
           <button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>

+ 90 - 1
data/web/inc/functions.dkim.inc.php

@@ -88,6 +88,84 @@ function dkim($_action, $_data = null) {
         return false;
         return false;
       }
       }
     break;
     break;
+    case 'import':
+      if ($_SESSION['mailcow_cc_role'] != "admin") {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      $private_key_input = trim($_data['private_key_file']);
+      $private_key_normalized = preg_replace('~\r\n?~', "\n", $private_key_input);
+      $private_key = openssl_pkey_get_private($private_key_normalized);
+      if ($ssl_error = openssl_error_string()) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Private key error: ' . $ssl_error
+        );
+        return false;
+      }
+      // Explode by nl
+      $pem_public_key_array = explode(PHP_EOL, trim(openssl_pkey_get_details($private_key)['key']));
+      // Remove first and last line/item
+      array_shift($pem_public_key_array);
+      array_pop($pem_public_key_array);
+      // Implode as single string
+      $pem_public_key = implode('', $pem_public_key_array);
+      $dkim_selector = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : 'dkim';
+      $domain	= $_data['domain'];
+      if (!is_valid_domain_name($domain)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+        );
+        return false;
+      }
+      if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+          );
+          return false;
+      }
+      if (!ctype_alnum($dkim_selector)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+        );
+        return false;
+      }
+      try {
+        $redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
+        $redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
+        $redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Redis: '.$e
+        );
+        return false;
+      }
+      unset($private_key_normalized);
+      unset($private_key);
+      unset($private_key_input);
+      try {
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Redis: '.$e
+        );
+        return false;
+      }
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['dkim_added'])
+      );
+      return true;
+    break;
     case 'details':
     case 'details':
       if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
       if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
         return false;
         return false;
@@ -95,7 +173,18 @@ function dkim($_action, $_data = null) {
       $dkimdata = array();
       $dkimdata = array();
       if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) {
       if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) {
         $dkimdata['pubkey'] = $redis_dkim_key_data;
         $dkimdata['pubkey'] = $redis_dkim_key_data;
-        $dkimdata['length'] = (strlen($dkimdata['pubkey']) < 391) ? 1024 : 2048;
+        if (strlen($dkimdata['pubkey']) < 391) {
+          $dkimdata['length'] = "1024";
+        }
+        elseif (strlen($dkimdata['pubkey']) < 736) {
+          $dkimdata['length'] = "2048";
+        }
+        elseif (strlen($dkimdata['pubkey']) < 1416) {
+          $dkimdata['length'] = "4096";
+        }
+        else {
+          $dkimdata['length'] = ">= 8192";
+        }
         $dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
         $dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
         $dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
         $dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
       }
       }

+ 93 - 0
data/web/inc/functions.fail2ban.inc.php

@@ -0,0 +1,93 @@
+<?php
+function fail2ban($_action, $_data = null) {
+  global $redis;
+  global $lang;
+  switch ($_action) {
+    case 'get':
+      $data = array();
+      if ($_SESSION['mailcow_cc_role'] != "admin") {
+        return false;
+      }
+      try {
+        $data['ban_time'] = $redis->Get('F2B_BAN_TIME');
+        $data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
+        $data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
+        $wl = $redis->hGetAll('F2B_WHITELIST');
+        if (is_array($wl)) {
+          foreach ($wl as $key => $value) {
+            $tmp_data[] = $key;
+          }
+          $data['whitelist'] = implode(PHP_EOL, $tmp_data);
+        }
+        else {
+          $data['whitelist'] = "";
+        }
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Redis: '.$e
+        );
+        return false;
+      }
+      return $data;
+    break;
+    case 'edit':
+      if ($_SESSION['mailcow_cc_role'] != "admin") {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      $is_now = fail2ban('get');
+      if (!empty($is_now)) {
+        $ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
+        $max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['active_int']);
+        $retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
+      }
+      else {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+      $wl = $_data['whitelist'];
+      $ban_time = ($ban_time < 60) ? 60 : $ban_time;
+      $max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
+      $retry_window = ($retry_window < 1) ? 1 : $retry_window;
+      try {
+        $redis->Set('F2B_BAN_TIME', $ban_time);
+        $redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
+        $redis->Set('F2B_RETRY_WINDOW', $retry_window);
+        $redis->Del('F2B_WHITELIST');
+        if(!empty($wl)) {
+          $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
+          if (is_array($wl_array)) {
+            foreach ($wl_array as $wl_item) {
+              $cidr = explode('/', $wl_item);
+              if (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 32))) {
+                $redis->hSet('F2B_WHITELIST', $wl_item, 1);
+              }
+              elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 128))) {
+                $redis->hSet('F2B_WHITELIST', $wl_item, 1);
+              }
+            }
+          }
+        }
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'Redis: '.$e
+        );
+        return false;
+      }
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['f2b_modified'])
+      );
+    break;
+  }
+}

+ 2 - 92
data/web/inc/functions.inc.php

@@ -229,11 +229,11 @@ function check_login($user, $pass) {
 	}
 	}
 	if (!isset($_SESSION['ldelay'])) {
 	if (!isset($_SESSION['ldelay'])) {
 		$_SESSION['ldelay'] = "0";
 		$_SESSION['ldelay'] = "0";
-    error_log("Mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+    error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
 	}
 	}
 	elseif (!isset($_SESSION['mailcow_cc_username'])) {
 	elseif (!isset($_SESSION['mailcow_cc_username'])) {
 		$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
 		$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
-		error_log("Mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+		error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
 	}
 	}
 	sleep($_SESSION['ldelay']);
 	sleep($_SESSION['ldelay']);
 }
 }
@@ -1435,94 +1435,4 @@ function get_logs($container, $lines = 100) {
   }
   }
   return false;
   return false;
 }
 }
-function get_f2b_parameters() {
-	global $lang;
-	global $redis;
-  $data = array();
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-		return false;
-	}
-  try {
-    $data['ban_time'] = $redis->Get('F2B_BAN_TIME');
-    $data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
-    $data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
-    $wl = $redis->hGetAll('F2B_WHITELIST');
-    if (is_array($wl)) {
-      foreach ($wl as $key => $value) {
-        $tmp_data[] = $key;
-      }
-      $data['whitelist'] = implode(PHP_EOL, $tmp_data);
-    }
-    else {
-      $data['whitelist'] = "";
-    }
-  }
-  catch (RedisException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'Redis: '.$e
-    );
-    return false;
-  }
-  return $data;
-}
-function edit_f2b_parameters($postarray) {
-	global $lang;
-	global $redis;
-	if ($_SESSION['mailcow_cc_role'] != "admin") {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  $is_now = get_f2b_parameters();
-  if (!empty($is_now)) {
-    $ban_time = intval((isset($postarray['ban_time'])) ? $postarray['ban_time'] : $is_now['ban_time']);
-    $max_attempts = intval((isset($postarray['max_attempts'])) ? $postarray['max_attempts'] : $is_now['active_int']);
-    $retry_window = intval((isset($postarray['retry_window'])) ? $postarray['retry_window'] : $is_now['retry_window']);
-  }
-  else {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => sprintf($lang['danger']['access_denied'])
-    );
-    return false;
-  }
-  $wl = $postarray['whitelist'];
-  $ban_time = ($ban_time < 60) ? 60 : $ban_time;
-  $max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
-  $retry_window = ($retry_window < 1) ? 1 : $retry_window;
-  try {
-    $redis->Set('F2B_BAN_TIME', $ban_time);
-    $redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
-    $redis->Set('F2B_RETRY_WINDOW', $retry_window);
-    $redis->Del('F2B_WHITELIST');
-    if(!empty($wl)) {
-      $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
-      if (is_array($wl_array)) {
-        foreach ($wl_array as $wl_item) {
-          $cidr = explode('/', $wl_item);
-          if (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 32))) {
-            $redis->hSet('F2B_WHITELIST', $wl_item, 1);
-          }
-          elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 128))) {
-            $redis->hSet('F2B_WHITELIST', $wl_item, 1);
-          }
-        }
-      }
-    }
-  }
-  catch (RedisException $e) {
-    $_SESSION['return'] = array(
-      'type' => 'danger',
-      'msg' => 'Redis: '.$e
-    );
-    return false;
-  }
-  $_SESSION['return'] = array(
-    'type' => 'success',
-    'msg' => 'Saved changes to Fail2ban configuration'
-  );
-}
 ?>
 ?>

+ 1 - 0
data/web/inc/prerequisites.inc.php

@@ -64,6 +64,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
 init_db_schema();
 init_db_schema();

+ 34 - 1
data/web/json_api.php

@@ -390,6 +390,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
               ));
             }
             }
           break;
           break;
+          case "dkim_import":
+            if (isset($_POST['attr'])) {
+              $attr = (array)json_decode($_POST['attr'], true);
+              if (dkim('import', $attr) === false) {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'error',
+                    'msg' => 'Cannot add item'
+                  ));
+                }
+              }
+              else {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'success',
+                    'msg' => 'Task completed'
+                  ));
+                }
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Cannot find attributes in post data'
+              ));
+            }
+          break;
           case "domain-admin":
           case "domain-admin":
             if (isset($_POST['attr'])) {
             if (isset($_POST['attr'])) {
               $attr = (array)json_decode($_POST['attr'], true);
               $attr = (array)json_decode($_POST['attr'], true);
@@ -1925,7 +1958,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
             // No items
             // No items
             if (isset($_POST['attr'])) {
             if (isset($_POST['attr'])) {
               $attr = (array)json_decode($_POST['attr'], true);
               $attr = (array)json_decode($_POST['attr'], true);
-              if (edit_f2b_parameters($attr) === false) {
+              if (fail2ban('edit', $attr) === false) {
                 if (isset($_SESSION['return'])) {
                 if (isset($_SESSION['return'])) {
                   echo json_encode($_SESSION['return']);
                   echo json_encode($_SESSION['return']);
                 }
                 }

+ 9 - 1
data/web/lang/lang.de.php

@@ -49,6 +49,7 @@ $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden ges
 $lang['success']['mailbox_modified'] = 'Änderungen an Mailbox %s wurden gespeichert';
 $lang['success']['mailbox_modified'] = 'Änderungen an Mailbox %s wurden gespeichert';
 $lang['success']['resource_modified'] = "Änderungen an Ressource %s wurden gespeichert";
 $lang['success']['resource_modified'] = "Änderungen an Ressource %s wurden gespeichert";
 $lang['success']['object_modified'] = "Änderungen an Objekt %s wurden gespeichert";
 $lang['success']['object_modified'] = "Änderungen an Objekt %s wurden gespeichert";
+$lang['success']['f2b_modified'] = "Änderungen an Fail2ban Parametern wurden gespeichert";
 $lang['success']['msg_size_saved'] = 'Limit wurde gesetzt';
 $lang['success']['msg_size_saved'] = 'Limit wurde gesetzt';
 $lang['danger']['aliasd_not_found'] = 'Alias-Domain nicht gefunden';
 $lang['danger']['aliasd_not_found'] = 'Alias-Domain nicht gefunden';
 $lang['danger']['targetd_not_found'] = 'Ziel-Domain nicht gefunden';
 $lang['danger']['targetd_not_found'] = 'Ziel-Domain nicht gefunden';
@@ -416,7 +417,14 @@ $lang['tfa']['scan_qr_code'] = "Bitte scannen Sie jetzt den angezeigten QR-Code:
 $lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel";
 $lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel";
 $lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens";
 $lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens";
 
 
-$lang['admin']['search_domain_da'] = 'Domains durchsuchen';
+$lang['admin']['private_key'] = 'Private Key';
+$lang['admin']['import'] = 'Importieren';
+$lang['admin']['import_private_key'] = 'Private Key importieren';
+$lang['admin']['f2b_parameters'] = 'Fail2ban Parameter';
+$lang['admin']['f2b_ban_time'] = 'Banzeit (s)';
+$lang['admin']['f2b_max_attempts'] = 'Max. Versuche';
+$lang['admin']['f2b_retry_window'] = 'Wiederholungen im Zeitraum von (s)';
+$lang['admin']['f2b_whitelist'] = 'Whitelist für Netzwerke und Hosts';
 $lang['admin']['restrictions'] = 'Postfix Restriktionen';
 $lang['admin']['restrictions'] = 'Postfix Restriktionen';
 $lang['admin']['rr'] = 'Postfix Empfänger Restriktionen';
 $lang['admin']['rr'] = 'Postfix Empfänger Restriktionen';
 $lang['admin']['sr'] = 'Postfix Sender Restriktionen';
 $lang['admin']['sr'] = 'Postfix Sender Restriktionen';

+ 9 - 0
data/web/lang/lang.en.php

@@ -51,6 +51,7 @@ $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved";
 $lang['success']['mailbox_modified'] = "Changes to mailbox %s have been saved";
 $lang['success']['mailbox_modified'] = "Changes to mailbox %s have been saved";
 $lang['success']['resource_modified'] = "Changes to mailbox %s have been saved";
 $lang['success']['resource_modified'] = "Changes to mailbox %s have been saved";
 $lang['success']['object_modified'] = "Changes to object %s have been saved";
 $lang['success']['object_modified'] = "Changes to object %s have been saved";
+$lang['success']['f2b_modified'] = "Changes to Fail2ban parameters have been saved";
 $lang['success']['msg_size_saved'] = "Message size limit has been set";
 $lang['success']['msg_size_saved'] = "Message size limit has been set";
 $lang['danger']['aliasd_not_found'] = "Alias domain not found";
 $lang['danger']['aliasd_not_found'] = "Alias domain not found";
 $lang['danger']['targetd_not_found'] = "Target domain not found";
 $lang['danger']['targetd_not_found'] = "Target domain not found";
@@ -421,6 +422,14 @@ $lang['tfa']['scan_qr_code'] = "Please scan the following code with your authent
 $lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes";
 $lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes";
 $lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token";
 $lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token";
 
 
+$lang['admin']['private_key'] = 'Private key';
+$lang['admin']['import'] = 'Import';
+$lang['admin']['import_private_key'] = 'Import private key';
+$lang['admin']['f2b_parameters'] = 'Fail2ban parameters';
+$lang['admin']['f2b_ban_time'] = 'Ban time (s)';
+$lang['admin']['f2b_max_attempts'] = 'Max. attempts';
+$lang['admin']['f2b_retry_window'] = 'Retry window (s) for max. attempts';
+$lang['admin']['f2b_whitelist'] = 'Whitelisted networks/hosts';
 $lang['admin']['search_domain_da'] = 'Search domains';
 $lang['admin']['search_domain_da'] = 'Search domains';
 $lang['admin']['restrictions'] = 'Postfix Restrictions';
 $lang['admin']['restrictions'] = 'Postfix Restrictions';
 $lang['admin']['rr'] = 'Postfix Recipient Restrictions';
 $lang['admin']['rr'] = 'Postfix Recipient Restrictions';

+ 10 - 10
docker-compose.yml

@@ -10,9 +10,9 @@ services:
           condition: service_healthy
           condition: service_healthy
       healthcheck:
       healthcheck:
         test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"]
         test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"]
-        interval: 3s
-        timeout: 3s
-        retries: 5
+        interval: 30s
+        timeout: 7s
+        retries: 10
       volumes:
       volumes:
         - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
         - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
       restart: always
       restart: always
@@ -28,8 +28,8 @@ services:
       healthcheck:
       healthcheck:
         test: ["CMD", "mysqladmin", "ping", "--host", "localhost", "--silent"]
         test: ["CMD", "mysqladmin", "ping", "--host", "localhost", "--silent"]
         interval: 10s
         interval: 10s
-        timeout: 30s
-        retries: 5
+        timeout: 7s
+        retries: 10
       volumes:
       volumes:
         - mysql-vol-1:/var/lib/mysql/
         - mysql-vol-1:/var/lib/mysql/
         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
@@ -171,7 +171,7 @@ services:
             - sogo
             - sogo
 
 
     dovecot-mailcow:
     dovecot-mailcow:
-      image: mailcow/dovecot:1.0
+      image: mailcow/dovecot:1.1
       build: ./data/Dockerfiles/dovecot
       build: ./data/Dockerfiles/dovecot
       depends_on:
       depends_on:
         unbound-mailcow:
         unbound-mailcow:
@@ -293,19 +293,19 @@ services:
     acme-mailcow:
     acme-mailcow:
       depends_on:
       depends_on:
         - nginx-mailcow
         - nginx-mailcow
-      image: mailcow/acme:1.8
+      image: mailcow/acme:1.9
       build: ./data/Dockerfiles/acme
       build: ./data/Dockerfiles/acme
       dns:
       dns:
         - 172.22.1.254
         - 172.22.1.254
       dns_search: mailcow-network
       dns_search: mailcow-network
       environment:
       environment:
-        - CONTAINERS_RESTART=mailcowdockerized_postfix-mailcow_1 mailcowdockerized_dovecot-mailcow_1 mailcowdockerized_nginx-mailcow_1
-        - ADDITIONAL_SAN=${ADDITIONAL_SAN}
+        - ADDITIONAL_SAN=${ADDITIONAL_SAN:- }
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
         - DBNAME=${DBNAME}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
         - DBPASS=${DBPASS}
         - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
         - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
+        - SKIP_IP_CHECK=${SKIP_IP_CHECK:-n}
       volumes:
       volumes:
         - ./data/web/.well-known/acme-challenge:/var/www/acme:rw
         - ./data/web/.well-known/acme-challenge:/var/www/acme:rw
         - ./data/assets/ssl:/var/lib/acme/:rw
         - ./data/assets/ssl:/var/lib/acme/:rw
@@ -319,7 +319,7 @@ services:
             - acme
             - acme
 
 
     fail2ban-mailcow:
     fail2ban-mailcow:
-      image: mailcow/fail2ban:1.3
+      image: mailcow/fail2ban:1.4
       build: ./data/Dockerfiles/fail2ban
       build: ./data/Dockerfiles/fail2ban
       depends_on:
       depends_on:
         - dovecot-mailcow
         - dovecot-mailcow

+ 3 - 0
generate_config.sh

@@ -81,6 +81,9 @@ ADDITIONAL_SAN=
 # To never run acme-mailcow for Let's Encrypt, set this to y
 # To never run acme-mailcow for Let's Encrypt, set this to y
 SKIP_LETS_ENCRYPT=n
 SKIP_LETS_ENCRYPT=n
 
 
+# Skip IPv4 check in ACME container
+SKIP_IP_CHECK=n
+
 # To never run fail2ban-mailcow
 # To never run fail2ban-mailcow
 SKIP_FAIL2BAN=n
 SKIP_FAIL2BAN=n