Merge, conflict fixed
commit
cf902854d7
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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" {
|
||||
|
@ -319,4 +236,4 @@ while ($row = array_shift($rows)) {
|
|||
<?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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue