Merge, conflict fixed

master
andryyy 2017-07-02 11:22:35 +02:00
commit cf902854d7
15 changed files with 431 additions and 316 deletions

View File

@ -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

View File

@ -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

View File

@ -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 \

View File

@ -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:

View File

@ -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)) {
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_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" {

View File

@ -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>

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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'
);
}
?>

View File

@ -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();

View File

@ -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']);
}

View File

@ -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';

View File

@ -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';

View File

@ -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

View File

@ -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