2
0
andryyy 8 жил өмнө
parent
commit
cf902854d7

+ 1 - 0
data/Dockerfiles/acme/Dockerfile

@@ -8,6 +8,7 @@ RUN apk add --update --no-cache \
 	curl \
 	openssl \
 	bind-tools \
+	jq \
 	mariadb-client
 
 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
 SSL_EXAMPLE=/var/lib/ssl-example
+
 mkdir -p ${ACME_BASE}/acme/private
 
 restart_containers(){
 	for container in $*; do
+		echo "Restarting ${container}..."
 		curl -X POST \
 			--unix-socket /var/run/docker.sock \
 			"http/containers/${container}/restart"
@@ -45,14 +47,14 @@ else
 			echo "Restoring previous acme certificate and restarting script..."
 			cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
 			cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
-			exec $(readlink -f "$0")
+			exec env TRIGGER_RESTART=1 $(readlink -f "$0")
 		fi
 	ISSUER="mailcow"
 	else
 		echo "Restoring mailcow snake-oil certificates and restarting script..."
 		cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem
 		cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem
-		exec $(readlink -f "$0")
+		exec env TRIGGER_RESTART=1 $(readlink -f "$0")
 	fi
 fi
 
@@ -66,6 +68,8 @@ while true; do
 	declare -a ADDITIONAL_VALIDATED_SAN
 	IFS=',' read -r -a ADDITIONAL_SAN_ARR <<< "${ADDITIONAL_SAN}"
 	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
 		SQL_DOMAIN_ARR+=("${line}")
@@ -75,7 +79,7 @@ while true; do
 		A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1)
 		if [[ ! -z ${A_CONFIG} ]]; then
 			echo "Found A record for autoconfig.${SQL_DOMAIN}: ${A_CONFIG}"
-			if [[ ${IPV4} == ${A_CONFIG} ]]; then
+			if [[ ${IPV4:-ERR} == ${A_CONFIG} ]]; then
 				echo "Confirmed A record autoconfig.${SQL_DOMAIN}"
 				VALIDATED_CONFIG_DOMAINS+=("autoconfig.${SQL_DOMAIN}")
 			else
@@ -88,7 +92,7 @@ while true; do
         A_DISCOVER=$(dig A autodiscover.${SQL_DOMAIN} +short | tail -n 1)
 		if [[ ! -z ${A_DISCOVER} ]]; then
 			echo "Found A record for autodiscover.${SQL_DOMAIN}: ${A_DISCOVER}"
-			if [[ ${IPV4} == ${A_DISCOVER} ]]; then
+			if [[ ${IPV4:-ERR} == ${A_DISCOVER} ]]; then
 				echo "Confirmed A record autodiscover.${SQL_DOMAIN}"
 				VALIDATED_CONFIG_DOMAINS+=("autodiscover.${SQL_DOMAIN}")
 			else
@@ -102,7 +106,7 @@ while true; do
 	A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1)
 	if [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then
 		echo "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}"
-		if [[ ${IPV4} == ${A_MAILCOW_HOSTNAME} ]]; then
+		if [[ ${IPV4:-ERR} == ${A_MAILCOW_HOSTNAME} ]]; then
 			echo "Confirmed A record ${MAILCOW_HOSTNAME}"
 			VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
 		else
@@ -116,7 +120,7 @@ while true; do
 		A_SAN=$(dig A ${SAN} +short | tail -n 1)
 		if [[ ! -z ${A_SAN} ]]; then
 			echo "Found A record for ${SAN}: ${A_SAN}"
-			if [[ ${IPV4} == ${A_SAN} ]]; then
+			if [[ ${IPV4:-ERR} == ${A_SAN} ]]; then
 				echo "Confirmed A record ${SAN}"
 				ADDITIONAL_VALIDATED_SAN+=("${SAN}")
 			else
@@ -127,7 +131,7 @@ while true; do
 		fi
 	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
 		echo "Cannot validate hostnames, skipping Let's Encrypt..."
 		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 ))
 	if [[ ! -z ${ORPHANED_SAN[*]} ]] && [[ ${ISSUER} != *"mailcow"* ]]; then
 		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/
 		[[ -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/
@@ -159,11 +163,11 @@ while true; do
 
 			# restart docker containers
 			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}/key.pem ${ACME_BASE}/key.pem
 			fi
-			restart_containers ${CONTAINERS_RESTART}
+			restart_containers ${CONTAINERS_RESTART[*]}
 			;;
 		1) # failure
 			if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then
@@ -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/privkey.pem ${ACME_BASE}/key.pem
 				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..."
 				cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.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
 				TRIGGER_RESTART=1
 			fi
-			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART}
+			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
 			exit 1;;
 		2) # no change
 			if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then
 				echo "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..."
 				cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
-				restart_containers ${CONTAINERS_RESTART}
+				restart_containers ${CONTAINERS_RESTART[*]}
 			fi
 			if ! verify_hash_match ${ACME_BASE}/cert.pem ${ACME_BASE}/key.pem; then
 				echo "Certificate was not changed, but hashes do not match, restoring from previous acme request and restarting containers..."
 				cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem
 				cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem
-				restart_containers ${CONTAINERS_RESTART}
+				restart_containers ${CONTAINERS_RESTART[*]}
 			fi
 			;;
 		*) # 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/privkey.pem ${ACME_BASE}/key.pem
 				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..."
 				cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.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
 				TRIGGER_RESTART=1
 			fi
-			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART}
+			[[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]}
 			exit 1;;
 	esac
 

+ 2 - 2
data/Dockerfiles/dovecot/Dockerfile

@@ -3,8 +3,8 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
 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 \
 	automake \

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

@@ -19,12 +19,33 @@ if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
 	raise SystemExit
 
 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_MAX_ATTEMPTS", "10")
@@ -135,12 +156,17 @@ def watch(container):
 	log['message'] = "Watching %s" % container
 	r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
 	print "Watching", container
-	client = docker.from_env()
 	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():
 	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 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');
 require_once "vars.inc.php";
 
-ini_set('error_reporting', 0);
+ini_set('error_reporting', 1);
 
 $dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
 $opt = [
@@ -29,6 +24,77 @@ catch (PDOException $e) {
   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 {
 <?php
@@ -44,73 +110,18 @@ while ($row = array_shift($rows)) {
 	$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
 ?>
 	score_<?=$username_sane;?> {
-		priority = low;
+		priority = 4;
+<?php
+  foreach (ucl_rcpts($row['object'], 'mailbox') as $rcpt) {
+?>
+		rcpt = "<?=$rcpt;?>";
 <?php
+  }
 	$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` 
 		WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
 			AND `object`= :object");
 	$stmt->execute(array(':object' => $row['object']));
 	$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" {
 			actions {
@@ -145,70 +156,23 @@ while ($row = array_shift($rows)) {
 <?php
 	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 ?>
-		priority = medium;
-		rcpt = "/.*@<?=$row['object'];?>/";
+		priority = 5;
 <?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
 		}
 	}
 	else {
 ?>
-		priority = high;
+		priority = 6;
 <?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
-	}
-	$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" {
@@ -243,70 +207,23 @@ while ($row = array_shift($rows)) {
 <?php
 	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
 ?>
-		priority = medium;
-		rcpt = "/.*@<?=$row['object'];?>/";
+		priority = 5;
 <?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
 		}
 	}
 	else {
 ?>
-		priority = high;
+		priority = 6;
 <?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
-    }
-?>
-		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" {
@@ -319,4 +236,4 @@ while ($row = array_shift($rows)) {
 <?php
 }
 ?>
-}
+}

+ 27 - 6
data/web/admin.php

@@ -230,6 +230,27 @@ $tfa_data = get_tfa();
           </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>
         </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>
     
@@ -271,26 +292,26 @@ $tfa_data = get_tfa();
     </div>
 
     <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">
       <?php
-      $f2b_data = get_f2b_parameters();
+      $f2b_data = fail2ban('get');
       ?>
         <form class="form" data-id="f2b" role="form" method="post">
           <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>
           </div>
           <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>
           </div>
           <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>
           </div>
           <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>
           </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>

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

@@ -88,6 +88,84 @@ function dkim($_action, $_data = null) {
         return false;
       }
     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':
       if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
         return false;
@@ -95,7 +173,18 @@ function dkim($_action, $_data = null) {
       $dkimdata = array();
       if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_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_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'])) {
 		$_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'])) {
 		$_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']);
 }
@@ -1435,94 +1435,4 @@ function get_logs($container, $lines = 100) {
   }
   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.dkim.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/triggers.inc.php';
 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;
+          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":
             if (isset($_POST['attr'])) {
               $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
             if (isset($_POST['attr'])) {
               $attr = (array)json_decode($_POST['attr'], true);
-              if (edit_f2b_parameters($attr) === false) {
+              if (fail2ban('edit', $attr) === false) {
                 if (isset($_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']['resource_modified'] = "Änderungen an Ressource %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['danger']['aliasd_not_found'] = 'Alias-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']['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']['rr'] = 'Postfix Empfänger 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']['resource_modified'] = "Changes to mailbox %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['danger']['aliasd_not_found'] = "Alias 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']['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']['restrictions'] = 'Postfix Restrictions';
 $lang['admin']['rr'] = 'Postfix Recipient Restrictions';

+ 10 - 10
docker-compose.yml

@@ -10,9 +10,9 @@ services:
           condition: service_healthy
       healthcheck:
         test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"]
-        interval: 3s
-        timeout: 3s
-        retries: 5
+        interval: 30s
+        timeout: 7s
+        retries: 10
       volumes:
         - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
       restart: always
@@ -28,8 +28,8 @@ services:
       healthcheck:
         test: ["CMD", "mysqladmin", "ping", "--host", "localhost", "--silent"]
         interval: 10s
-        timeout: 30s
-        retries: 5
+        timeout: 7s
+        retries: 10
       volumes:
         - mysql-vol-1:/var/lib/mysql/
         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro
@@ -171,7 +171,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.0
+      image: mailcow/dovecot:1.1
       build: ./data/Dockerfiles/dovecot
       depends_on:
         unbound-mailcow:
@@ -293,19 +293,19 @@ services:
     acme-mailcow:
       depends_on:
         - nginx-mailcow
-      image: mailcow/acme:1.8
+      image: mailcow/acme:1.9
       build: ./data/Dockerfiles/acme
       dns:
         - 172.22.1.254
       dns_search: mailcow-network
       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}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
         - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
+        - SKIP_IP_CHECK=${SKIP_IP_CHECK:-n}
       volumes:
         - ./data/web/.well-known/acme-challenge:/var/www/acme:rw
         - ./data/assets/ssl:/var/lib/acme/:rw
@@ -319,7 +319,7 @@ services:
             - acme
 
     fail2ban-mailcow:
-      image: mailcow/fail2ban:1.3
+      image: mailcow/fail2ban:1.4
       build: ./data/Dockerfiles/fail2ban
       depends_on:
         - 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
 SKIP_LETS_ENCRYPT=n
 
+# Skip IPv4 check in ACME container
+SKIP_IP_CHECK=n
+
 # To never run fail2ban-mailcow
 SKIP_FAIL2BAN=n