diff --git a/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf b/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf
index ea2bcfbf..f8d4d74b 100644
--- a/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf
+++ b/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf
@@ -22,12 +22,12 @@ destination d_redis_ui_log {
command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
-destination d_redis_f2b_channel {
+destination d_redis_netfilter_channel {
redis(
host("`REDIS_SLAVEOF_IP`")
persist-name("redis2")
port(`REDIS_SLAVEOF_PORT`)
- command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
+ command("PUBLISH" "NETFILTER_CHANNEL" "$(sanitize $MESSAGE)")
);
};
filter f_mail { facility(mail); };
@@ -41,5 +41,5 @@ log {
destination(d_stdout);
filter(f_mail);
destination(d_redis_ui_log);
- destination(d_redis_f2b_channel);
+ destination(d_redis_netfilter_channel);
};
diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf
index 2ee4f624..79836863 100644
--- a/data/Dockerfiles/dovecot/syslog-ng.conf
+++ b/data/Dockerfiles/dovecot/syslog-ng.conf
@@ -22,12 +22,12 @@ destination d_redis_ui_log {
command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
-destination d_redis_f2b_channel {
+destination d_redis_netfilter_channel {
redis(
host("redis-mailcow")
persist-name("redis2")
port(6379)
- command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
+ command("PUBLISH" "NETFILTER_CHANNEL" "$(sanitize $MESSAGE)")
);
};
filter f_mail { facility(mail); };
@@ -41,5 +41,5 @@ log {
destination(d_stdout);
filter(f_mail);
destination(d_redis_ui_log);
- destination(d_redis_f2b_channel);
+ destination(d_redis_netfilter_channel);
};
diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py
index 1ccc150e..9a6e5965 100644
--- a/data/Dockerfiles/netfilter/server.py
+++ b/data/Dockerfiles/netfilter/server.py
@@ -1,587 +1,587 @@
-#!/usr/bin/env python3
-
-import re
-import os
-import sys
-import time
-import atexit
-import signal
-import ipaddress
-from collections import Counter
-from random import randint
-from threading import Thread
-from threading import Lock
-import redis
-import json
-import iptc
-import dns.resolver
-import dns.exception
-
-while True:
- try:
- redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
- redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
- if "".__eq__(redis_slaveof_ip):
- r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
- else:
- r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
- r.ping()
- except Exception as ex:
- print('%s - trying again in 3 seconds' % (ex))
- time.sleep(3)
- else:
- break
-
-pubsub = r.pubsub()
-
-WHITELIST = []
-BLACKLIST= []
-
-bans = {}
-
-quit_now = False
-exit_code = 0
-lock = Lock()
-
-def log(priority, message):
- tolog = {}
- tolog['time'] = int(round(time.time()))
- tolog['priority'] = priority
- tolog['message'] = message
- r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
- print(message)
-
-def logWarn(message):
- log('warn', message)
-
-def logCrit(message):
- log('crit', message)
-
-def logInfo(message):
- log('info', message)
-
-def refreshF2boptions():
- global f2boptions
- global quit_now
- global exit_code
- if not r.get('F2B_OPTIONS'):
- f2boptions = {}
- f2boptions['ban_time'] = int
- f2boptions['max_attempts'] = int
- f2boptions['retry_window'] = int
- f2boptions['netban_ipv4'] = int
- f2boptions['netban_ipv6'] = int
- f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
- f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
- f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
- f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
- f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
- r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
- else:
- try:
- f2boptions = {}
- f2boptions = json.loads(r.get('F2B_OPTIONS'))
- except ValueError:
- print('Error loading F2B options: F2B_OPTIONS is not json')
- quit_now = True
- exit_code = 2
-
-def refreshF2bregex():
- global f2bregex
- global quit_now
- global exit_code
- if not r.get('F2B_REGEX'):
- f2bregex = {}
- f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
- f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
- f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
- f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
- f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
- f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
- f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
- f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
- f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
- f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
- r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
- else:
- try:
- f2bregex = {}
- f2bregex = json.loads(r.get('F2B_REGEX'))
- except ValueError:
- print('Error loading F2B options: F2B_REGEX is not json')
- quit_now = True
- exit_code = 2
-
-if r.exists('F2B_LOG'):
- r.rename('F2B_LOG', 'NETFILTER_LOG')
-
-def mailcowChainOrder():
- global lock
- global quit_now
- global exit_code
- while not quit_now:
- time.sleep(10)
- with lock:
- filter4_table = iptc.Table(iptc.Table.FILTER)
- filter6_table = iptc.Table6(iptc.Table6.FILTER)
- filter4_table.refresh()
- filter6_table.refresh()
- for f in [filter4_table, filter6_table]:
- forward_chain = iptc.Chain(f, 'FORWARD')
- input_chain = iptc.Chain(f, 'INPUT')
- for chain in [forward_chain, input_chain]:
- target_found = False
- for position, item in enumerate(chain.rules):
- if item.target.name == 'MAILCOW':
- target_found = True
- if position > 2:
- logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
- quit_now = True
- exit_code = 2
- if not target_found:
- logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
- quit_now = True
- exit_code = 2
-
-def ban(address):
- global lock
- refreshF2boptions()
- BAN_TIME = int(f2boptions['ban_time'])
- MAX_ATTEMPTS = int(f2boptions['max_attempts'])
- RETRY_WINDOW = int(f2boptions['retry_window'])
- NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
- NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
-
- ip = ipaddress.ip_address(address)
- if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
- ip = ip.ipv4_mapped
- address = str(ip)
- if ip.is_private or ip.is_loopback:
- return
-
- self_network = ipaddress.ip_network(address)
-
- with lock:
- temp_whitelist = set(WHITELIST)
-
- if temp_whitelist:
- for wl_key in temp_whitelist:
- wl_net = ipaddress.ip_network(wl_key, False)
- if wl_net.overlaps(self_network):
- logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
- return
-
- net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
- net = str(net)
-
- if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
- bans[net] = { 'attempts': 0 }
- active_window = RETRY_WINDOW
- else:
- active_window = time.time() - bans[net]['last_attempt']
-
- bans[net]['attempts'] += 1
- bans[net]['last_attempt'] = time.time()
-
- active_window = time.time() - bans[net]['last_attempt']
-
- if bans[net]['attempts'] >= MAX_ATTEMPTS:
- cur_time = int(round(time.time()))
- logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
- if type(ip) is ipaddress.IPv4Address:
- with lock:
- chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
- rule = iptc.Rule()
- rule.src = net
- target = iptc.Target(rule, "REJECT")
- rule.target = target
- if rule not in chain.rules:
- chain.insert_rule(rule)
- else:
- with lock:
- chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
- rule = iptc.Rule6()
- rule.src = net
- target = iptc.Target(rule, "REJECT")
- rule.target = target
- if rule not in chain.rules:
- chain.insert_rule(rule)
- r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
- else:
- logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
-
-def unban(net):
- global lock
- if not net in bans:
- logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
- r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
- return
- logInfo('Unbanning %s' % net)
- if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
- with lock:
- chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
- rule = iptc.Rule()
- rule.src = net
- target = iptc.Target(rule, "REJECT")
- rule.target = target
- if rule in chain.rules:
- chain.delete_rule(rule)
- else:
- with lock:
- chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
- rule = iptc.Rule6()
- rule.src = net
- target = iptc.Target(rule, "REJECT")
- rule.target = target
- if rule in chain.rules:
- chain.delete_rule(rule)
- r.hdel('F2B_ACTIVE_BANS', '%s' % net)
- r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
- if net in bans:
- del bans[net]
-
-def permBan(net, unban=False):
- global lock
- if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
- with lock:
- chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
- rule = iptc.Rule()
- rule.src = net
- target = iptc.Target(rule, "REJECT")
- rule.target = target
- if rule not in chain.rules and not unban:
- logCrit('Add host/network %s to blacklist' % net)
- chain.insert_rule(rule)
- r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
- elif rule in chain.rules and unban:
- logCrit('Remove host/network %s from blacklist' % net)
- chain.delete_rule(rule)
- r.hdel('F2B_PERM_BANS', '%s' % net)
- else:
- with lock:
- chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
- rule = iptc.Rule6()
- rule.src = net
- target = iptc.Target(rule, "REJECT")
- rule.target = target
- if rule not in chain.rules and not unban:
- logCrit('Add host/network %s to blacklist' % net)
- chain.insert_rule(rule)
- r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
- elif rule in chain.rules and unban:
- logCrit('Remove host/network %s from blacklist' % net)
- chain.delete_rule(rule)
- r.hdel('F2B_PERM_BANS', '%s' % net)
-
-def quit(signum, frame):
- global quit_now
- quit_now = True
-
-def clear():
- global lock
- logInfo('Clearing all bans')
- for net in bans.copy():
- unban(net)
- with lock:
- filter4_table = iptc.Table(iptc.Table.FILTER)
- filter6_table = iptc.Table6(iptc.Table6.FILTER)
- for filter_table in [filter4_table, filter6_table]:
- filter_table.autocommit = False
- forward_chain = iptc.Chain(filter_table, "FORWARD")
- input_chain = iptc.Chain(filter_table, "INPUT")
- mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
- if mailcow_chain in filter_table.chains:
- for rule in mailcow_chain.rules:
- mailcow_chain.delete_rule(rule)
- for rule in forward_chain.rules:
- if rule.target.name == 'MAILCOW':
- forward_chain.delete_rule(rule)
- for rule in input_chain.rules:
- if rule.target.name == 'MAILCOW':
- input_chain.delete_rule(rule)
- filter_table.delete_chain("MAILCOW")
- filter_table.commit()
- filter_table.refresh()
- filter_table.autocommit = True
- r.delete('F2B_ACTIVE_BANS')
- r.delete('F2B_PERM_BANS')
- pubsub.unsubscribe()
-
-def watch():
- logInfo('Watching Redis channel F2B_CHANNEL')
- pubsub.subscribe('F2B_CHANNEL')
-
- global quit_now
- global exit_code
-
- while not quit_now:
- try:
- for item in pubsub.listen():
- refreshF2bregex()
- for rule_id, rule_regex in f2bregex.items():
- if item['data'] and item['type'] == 'message':
- try:
- result = re.search(rule_regex, item['data'])
- except re.error:
- result = False
- if result:
- addr = result.group(1)
- ip = ipaddress.ip_address(addr)
- if ip.is_private or ip.is_loopback:
- continue
- logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
- ban(addr)
- except Exception as ex:
- logWarn('Error reading log line from pubsub')
- quit_now = True
- exit_code = 2
-
-def snat4(snat_target):
- global lock
- global quit_now
-
- def get_snat4_rule():
- rule = iptc.Rule()
- rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
- rule.dst = '!' + rule.src
- target = rule.create_target("SNAT")
- target.to_source = snat_target
- match = rule.create_match("comment")
- match.comment = f'{int(round(time.time()))}'
- return rule
-
- while not quit_now:
- time.sleep(10)
- with lock:
- try:
- table = iptc.Table('nat')
- table.refresh()
- chain = iptc.Chain(table, 'POSTROUTING')
- table.autocommit = False
- new_rule = get_snat4_rule()
- for position, rule in enumerate(chain.rules):
- match = all((
- new_rule.get_src() == rule.get_src(),
- new_rule.get_dst() == rule.get_dst(),
- new_rule.target.parameters == rule.target.parameters,
- new_rule.target.name == rule.target.name
- ))
- if position == 0:
- if not match:
- logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
- chain.insert_rule(new_rule)
- else:
- if match:
- logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
- chain.delete_rule(rule)
- table.commit()
- table.autocommit = True
- except:
- print('Error running SNAT4, retrying...')
-
-def snat6(snat_target):
- global lock
- global quit_now
-
- def get_snat6_rule():
- rule = iptc.Rule6()
- rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
- rule.dst = '!' + rule.src
- target = rule.create_target("SNAT")
- target.to_source = snat_target
- return rule
-
- while not quit_now:
- time.sleep(10)
- with lock:
- try:
- table = iptc.Table6('nat')
- table.refresh()
- chain = iptc.Chain(table, 'POSTROUTING')
- table.autocommit = False
- if get_snat6_rule() not in chain.rules:
- logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
- chain.insert_rule(get_snat6_rule())
- table.commit()
- else:
- for position, item in enumerate(chain.rules):
- if item == get_snat6_rule():
- if position != 0:
- chain.delete_rule(get_snat6_rule())
- table.commit()
- table.autocommit = True
- except:
- print('Error running SNAT6, retrying...')
-
-def autopurge():
- while not quit_now:
- time.sleep(10)
- refreshF2boptions()
- BAN_TIME = int(f2boptions['ban_time'])
- MAX_ATTEMPTS = int(f2boptions['max_attempts'])
- QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
- if QUEUE_UNBAN:
- for net in QUEUE_UNBAN:
- unban(str(net))
- for net in bans.copy():
- if bans[net]['attempts'] >= MAX_ATTEMPTS:
- if time.time() - bans[net]['last_attempt'] > BAN_TIME:
- unban(net)
-
-def isIpNetwork(address):
- try:
- ipaddress.ip_network(address, False)
- except ValueError:
- return False
- return True
-
-
-def genNetworkList(list):
- resolver = dns.resolver.Resolver()
- hostnames = []
- networks = []
- for key in list:
- if isIpNetwork(key):
- networks.append(key)
- else:
- hostnames.append(key)
- for hostname in hostnames:
- hostname_ips = []
- for rdtype in ['A', 'AAAA']:
- try:
- answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
- except dns.exception.Timeout:
- logInfo('Hostname %s timedout on resolve' % hostname)
- break
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
- continue
- except dns.exception.DNSException as dnsexception:
- logInfo('%s' % dnsexception)
- continue
- for rdata in answer:
- hostname_ips.append(rdata.to_text())
- networks.extend(hostname_ips)
- return set(networks)
-
-def whitelistUpdate():
- global lock
- global quit_now
- global WHITELIST
- while not quit_now:
- start_time = time.time()
- list = r.hgetall('F2B_WHITELIST')
- new_whitelist = []
- if list:
- new_whitelist = genNetworkList(list)
- with lock:
- if Counter(new_whitelist) != Counter(WHITELIST):
- WHITELIST = new_whitelist
- logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
- time.sleep(60.0 - ((time.time() - start_time) % 60.0))
-
-def blacklistUpdate():
- global quit_now
- global BLACKLIST
- while not quit_now:
- start_time = time.time()
- list = r.hgetall('F2B_BLACKLIST')
- new_blacklist = []
- if list:
- new_blacklist = genNetworkList(list)
- if Counter(new_blacklist) != Counter(BLACKLIST):
- addban = set(new_blacklist).difference(BLACKLIST)
- delban = set(BLACKLIST).difference(new_blacklist)
- BLACKLIST = new_blacklist
- logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
- if addban:
- for net in addban:
- permBan(net=net)
- if delban:
- for net in delban:
- permBan(net=net, unban=True)
- time.sleep(60.0 - ((time.time() - start_time) % 60.0))
-
-def initChain():
- # Is called before threads start, no locking
- print("Initializing mailcow netfilter chain")
- # IPv4
- if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
- iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
- for c in ['FORWARD', 'INPUT']:
- chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
- rule = iptc.Rule()
- rule.src = '0.0.0.0/0'
- rule.dst = '0.0.0.0/0'
- target = iptc.Target(rule, "MAILCOW")
- rule.target = target
- if rule not in chain.rules:
- chain.insert_rule(rule)
- # IPv6
- if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
- iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
- for c in ['FORWARD', 'INPUT']:
- chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
- rule = iptc.Rule6()
- rule.src = '::/0'
- rule.dst = '::/0'
- target = iptc.Target(rule, "MAILCOW")
- rule.target = target
- if rule not in chain.rules:
- chain.insert_rule(rule)
-
-if __name__ == '__main__':
-
- # In case a previous session was killed without cleanup
- clear()
- # Reinit MAILCOW chain
- initChain()
-
- watch_thread = Thread(target=watch)
- watch_thread.daemon = True
- watch_thread.start()
-
- if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
- try:
- snat_ip = os.getenv('SNAT_TO_SOURCE')
- snat_ipo = ipaddress.ip_address(snat_ip)
- if type(snat_ipo) is ipaddress.IPv4Address:
- snat4_thread = Thread(target=snat4,args=(snat_ip,))
- snat4_thread.daemon = True
- snat4_thread.start()
- except ValueError:
- print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
-
- if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
- try:
- snat_ip = os.getenv('SNAT6_TO_SOURCE')
- snat_ipo = ipaddress.ip_address(snat_ip)
- if type(snat_ipo) is ipaddress.IPv6Address:
- snat6_thread = Thread(target=snat6,args=(snat_ip,))
- snat6_thread.daemon = True
- snat6_thread.start()
- except ValueError:
- print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
-
- autopurge_thread = Thread(target=autopurge)
- autopurge_thread.daemon = True
- autopurge_thread.start()
-
- mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
- mailcowchainwatch_thread.daemon = True
- mailcowchainwatch_thread.start()
-
- blacklistupdate_thread = Thread(target=blacklistUpdate)
- blacklistupdate_thread.daemon = True
- blacklistupdate_thread.start()
-
- whitelistupdate_thread = Thread(target=whitelistUpdate)
- whitelistupdate_thread.daemon = True
- whitelistupdate_thread.start()
-
- signal.signal(signal.SIGTERM, quit)
- atexit.register(clear)
-
- while not quit_now:
- time.sleep(0.5)
-
- sys.exit(exit_code)
+#!/usr/bin/env python3
+
+import re
+import os
+import sys
+import time
+import atexit
+import signal
+import ipaddress
+from collections import Counter
+from random import randint
+from threading import Thread
+from threading import Lock
+import redis
+import json
+import iptc
+import dns.resolver
+import dns.exception
+
+while True:
+ try:
+ redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
+ redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
+ if "".__eq__(redis_slaveof_ip):
+ r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
+ else:
+ r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
+ r.ping()
+ except Exception as ex:
+ print('%s - trying again in 3 seconds' % (ex))
+ time.sleep(3)
+ else:
+ break
+
+pubsub = r.pubsub()
+
+WHITELIST = []
+BLACKLIST= []
+
+bans = {}
+
+quit_now = False
+exit_code = 0
+lock = Lock()
+
+def log(priority, message):
+ tolog = {}
+ tolog['time'] = int(round(time.time()))
+ tolog['priority'] = priority
+ tolog['message'] = message
+ r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
+ print(message)
+
+def logWarn(message):
+ log('warn', message)
+
+def logCrit(message):
+ log('crit', message)
+
+def logInfo(message):
+ log('info', message)
+
+def refreshNetfilterOptions():
+ global netfilterOptions
+ global quit_now
+ global exit_code
+ if not r.get('NETFILTER_OPTIONS'):
+ netfilterOptions = {}
+ netfilterOptions['ban_time'] = int
+ netfilterOptions['max_attempts'] = int
+ netfilterOptions['retry_window'] = int
+ netfilterOptions['netban_ipv4'] = int
+ netfilterOptions['netban_ipv6'] = int
+ netfilterOptions['ban_time'] = r.get('NETFILTER_BAN_TIME') or 1800
+ netfilterOptions['max_attempts'] = r.get('NETFILTER_MAX_ATTEMPTS') or 10
+ netfilterOptions['retry_window'] = r.get('NETFILTER_RETRY_WINDOW') or 600
+ netfilterOptions['netban_ipv4'] = r.get('NETFILTER_NETBAN_IPV4') or 32
+ netfilterOptions['netban_ipv6'] = r.get('NETFILTER_NETBAN_IPV6') or 128
+ r.set('NETFILTER_OPTIONS', json.dumps(netfilterOptions, ensure_ascii=False))
+ else:
+ try:
+ netfilterOptions = {}
+ netfilterOptions = json.loads(r.get('NETFILTER_OPTIONS'))
+ except ValueError:
+ print('Error loading NETFILTER options: NETFILTER_OPTIONS is not json')
+ quit_now = True
+ exit_code = 2
+
+def refreshNetfilterRegex():
+ global netfilterRegex
+ global quit_now
+ global exit_code
+ if not r.get('NETFILTER_REGEX'):
+ netfilterRegex = {}
+ netfilterRegex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
+ netfilterRegex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
+ netfilterRegex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
+ netfilterRegex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
+ netfilterRegex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
+ netfilterRegex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
+ netfilterRegex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+ netfilterRegex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+ netfilterRegex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
+ netfilterRegex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
+ r.set('NETFILTER_REGEX', json.dumps(netfilterRegex, ensure_ascii=False))
+ else:
+ try:
+ netfilterRegex = {}
+ netfilterRegex = json.loads(r.get('NETFILTER_REGEX'))
+ except ValueError:
+ print('Error loading NETFILTER options: NETFILTER_REGEX is not json')
+ quit_now = True
+ exit_code = 2
+
+if r.exists('NETFILTER_LOG'):
+ r.rename('NETFILTER_LOG', 'NETFILTER_LOG')
+
+def mailcowChainOrder():
+ global lock
+ global quit_now
+ global exit_code
+ while not quit_now:
+ time.sleep(10)
+ with lock:
+ filter4_table = iptc.Table(iptc.Table.FILTER)
+ filter6_table = iptc.Table6(iptc.Table6.FILTER)
+ filter4_table.refresh()
+ filter6_table.refresh()
+ for f in [filter4_table, filter6_table]:
+ forward_chain = iptc.Chain(f, 'FORWARD')
+ input_chain = iptc.Chain(f, 'INPUT')
+ for chain in [forward_chain, input_chain]:
+ target_found = False
+ for position, item in enumerate(chain.rules):
+ if item.target.name == 'MAILCOW':
+ target_found = True
+ if position > 2:
+ logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
+ quit_now = True
+ exit_code = 2
+ if not target_found:
+ logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
+ quit_now = True
+ exit_code = 2
+
+def ban(address):
+ global lock
+ refreshNetfilterOptions()
+ BAN_TIME = int(netfilterOptions['ban_time'])
+ MAX_ATTEMPTS = int(netfilterOptions['max_attempts'])
+ RETRY_WINDOW = int(netfilterOptions['retry_window'])
+ NETBAN_IPV4 = '/' + str(netfilterOptions['netban_ipv4'])
+ NETBAN_IPV6 = '/' + str(netfilterOptions['netban_ipv6'])
+
+ ip = ipaddress.ip_address(address)
+ if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
+ ip = ip.ipv4_mapped
+ address = str(ip)
+ if ip.is_private or ip.is_loopback:
+ return
+
+ self_network = ipaddress.ip_network(address)
+
+ with lock:
+ temp_whitelist = set(WHITELIST)
+
+ if temp_whitelist:
+ for wl_key in temp_whitelist:
+ wl_net = ipaddress.ip_network(wl_key, False)
+ if wl_net.overlaps(self_network):
+ logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
+ return
+
+ net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
+ net = str(net)
+
+ if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
+ bans[net] = { 'attempts': 0 }
+ active_window = RETRY_WINDOW
+ else:
+ active_window = time.time() - bans[net]['last_attempt']
+
+ bans[net]['attempts'] += 1
+ bans[net]['last_attempt'] = time.time()
+
+ active_window = time.time() - bans[net]['last_attempt']
+
+ if bans[net]['attempts'] >= MAX_ATTEMPTS:
+ cur_time = int(round(time.time()))
+ logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
+ if type(ip) is ipaddress.IPv4Address:
+ with lock:
+ chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+ rule = iptc.Rule()
+ rule.src = net
+ target = iptc.Target(rule, "REJECT")
+ rule.target = target
+ if rule not in chain.rules:
+ chain.insert_rule(rule)
+ else:
+ with lock:
+ chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
+ rule = iptc.Rule6()
+ rule.src = net
+ target = iptc.Target(rule, "REJECT")
+ rule.target = target
+ if rule not in chain.rules:
+ chain.insert_rule(rule)
+ r.hset('NETFILTER_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
+ else:
+ logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
+
+def unban(net):
+ global lock
+ if not net in bans:
+ logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
+ r.hdel('NETFILTER_QUEUE_UNBAN', '%s' % net)
+ return
+ logInfo('Unbanning %s' % net)
+ if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
+ with lock:
+ chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+ rule = iptc.Rule()
+ rule.src = net
+ target = iptc.Target(rule, "REJECT")
+ rule.target = target
+ if rule in chain.rules:
+ chain.delete_rule(rule)
+ else:
+ with lock:
+ chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
+ rule = iptc.Rule6()
+ rule.src = net
+ target = iptc.Target(rule, "REJECT")
+ rule.target = target
+ if rule in chain.rules:
+ chain.delete_rule(rule)
+ r.hdel('NETFILTER_ACTIVE_BANS', '%s' % net)
+ r.hdel('NETFILTER_QUEUE_UNBAN', '%s' % net)
+ if net in bans:
+ del bans[net]
+
+def permBan(net, unban=False):
+ global lock
+ if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
+ with lock:
+ chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+ rule = iptc.Rule()
+ rule.src = net
+ target = iptc.Target(rule, "REJECT")
+ rule.target = target
+ if rule not in chain.rules and not unban:
+ logCrit('Add host/network %s to blacklist' % net)
+ chain.insert_rule(rule)
+ r.hset('NETFILTER_PERM_BANS', '%s' % net, int(round(time.time())))
+ elif rule in chain.rules and unban:
+ logCrit('Remove host/network %s from blacklist' % net)
+ chain.delete_rule(rule)
+ r.hdel('NETFILTER_PERM_BANS', '%s' % net)
+ else:
+ with lock:
+ chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
+ rule = iptc.Rule6()
+ rule.src = net
+ target = iptc.Target(rule, "REJECT")
+ rule.target = target
+ if rule not in chain.rules and not unban:
+ logCrit('Add host/network %s to blacklist' % net)
+ chain.insert_rule(rule)
+ r.hset('NETFILTER_PERM_BANS', '%s' % net, int(round(time.time())))
+ elif rule in chain.rules and unban:
+ logCrit('Remove host/network %s from blacklist' % net)
+ chain.delete_rule(rule)
+ r.hdel('NETFILTER_PERM_BANS', '%s' % net)
+
+def quit(signum, frame):
+ global quit_now
+ quit_now = True
+
+def clear():
+ global lock
+ logInfo('Clearing all bans')
+ for net in bans.copy():
+ unban(net)
+ with lock:
+ filter4_table = iptc.Table(iptc.Table.FILTER)
+ filter6_table = iptc.Table6(iptc.Table6.FILTER)
+ for filter_table in [filter4_table, filter6_table]:
+ filter_table.autocommit = False
+ forward_chain = iptc.Chain(filter_table, "FORWARD")
+ input_chain = iptc.Chain(filter_table, "INPUT")
+ mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
+ if mailcow_chain in filter_table.chains:
+ for rule in mailcow_chain.rules:
+ mailcow_chain.delete_rule(rule)
+ for rule in forward_chain.rules:
+ if rule.target.name == 'MAILCOW':
+ forward_chain.delete_rule(rule)
+ for rule in input_chain.rules:
+ if rule.target.name == 'MAILCOW':
+ input_chain.delete_rule(rule)
+ filter_table.delete_chain("MAILCOW")
+ filter_table.commit()
+ filter_table.refresh()
+ filter_table.autocommit = True
+ r.delete('NETFILTER_ACTIVE_BANS')
+ r.delete('NETFILTER_PERM_BANS')
+ pubsub.unsubscribe()
+
+def watch():
+ logInfo('Watching Redis channel NETFILTER_CHANNEL')
+ pubsub.subscribe('NETFILTER_CHANNEL')
+
+ global quit_now
+ global exit_code
+
+ while not quit_now:
+ try:
+ for item in pubsub.listen():
+ refreshNetfilterRegex()
+ for rule_id, rule_regex in netfilterRegex.items():
+ if item['data'] and item['type'] == 'message':
+ try:
+ result = re.search(rule_regex, item['data'])
+ except re.error:
+ result = False
+ if result:
+ addr = result.group(1)
+ ip = ipaddress.ip_address(addr)
+ if ip.is_private or ip.is_loopback:
+ continue
+ logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
+ ban(addr)
+ except Exception as ex:
+ logWarn('Error reading log line from pubsub')
+ quit_now = True
+ exit_code = 2
+
+def snat4(snat_target):
+ global lock
+ global quit_now
+
+ def get_snat4_rule():
+ rule = iptc.Rule()
+ rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
+ rule.dst = '!' + rule.src
+ target = rule.create_target("SNAT")
+ target.to_source = snat_target
+ match = rule.create_match("comment")
+ match.comment = f'{int(round(time.time()))}'
+ return rule
+
+ while not quit_now:
+ time.sleep(10)
+ with lock:
+ try:
+ table = iptc.Table('nat')
+ table.refresh()
+ chain = iptc.Chain(table, 'POSTROUTING')
+ table.autocommit = False
+ new_rule = get_snat4_rule()
+ for position, rule in enumerate(chain.rules):
+ match = all((
+ new_rule.get_src() == rule.get_src(),
+ new_rule.get_dst() == rule.get_dst(),
+ new_rule.target.parameters == rule.target.parameters,
+ new_rule.target.name == rule.target.name
+ ))
+ if position == 0:
+ if not match:
+ logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
+ chain.insert_rule(new_rule)
+ else:
+ if match:
+ logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
+ chain.delete_rule(rule)
+ table.commit()
+ table.autocommit = True
+ except:
+ print('Error running SNAT4, retrying...')
+
+def snat6(snat_target):
+ global lock
+ global quit_now
+
+ def get_snat6_rule():
+ rule = iptc.Rule6()
+ rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
+ rule.dst = '!' + rule.src
+ target = rule.create_target("SNAT")
+ target.to_source = snat_target
+ return rule
+
+ while not quit_now:
+ time.sleep(10)
+ with lock:
+ try:
+ table = iptc.Table6('nat')
+ table.refresh()
+ chain = iptc.Chain(table, 'POSTROUTING')
+ table.autocommit = False
+ if get_snat6_rule() not in chain.rules:
+ logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
+ chain.insert_rule(get_snat6_rule())
+ table.commit()
+ else:
+ for position, item in enumerate(chain.rules):
+ if item == get_snat6_rule():
+ if position != 0:
+ chain.delete_rule(get_snat6_rule())
+ table.commit()
+ table.autocommit = True
+ except:
+ print('Error running SNAT6, retrying...')
+
+def autopurge():
+ while not quit_now:
+ time.sleep(10)
+ refreshNetfilterOptions()
+ BAN_TIME = int(netfilterOptions['ban_time'])
+ MAX_ATTEMPTS = int(netfilterOptions['max_attempts'])
+ QUEUE_UNBAN = r.hgetall('NETFILTER_QUEUE_UNBAN')
+ if QUEUE_UNBAN:
+ for net in QUEUE_UNBAN:
+ unban(str(net))
+ for net in bans.copy():
+ if bans[net]['attempts'] >= MAX_ATTEMPTS:
+ if time.time() - bans[net]['last_attempt'] > BAN_TIME:
+ unban(net)
+
+def isIpNetwork(address):
+ try:
+ ipaddress.ip_network(address, False)
+ except ValueError:
+ return False
+ return True
+
+
+def genNetworkList(list):
+ resolver = dns.resolver.Resolver()
+ hostnames = []
+ networks = []
+ for key in list:
+ if isIpNetwork(key):
+ networks.append(key)
+ else:
+ hostnames.append(key)
+ for hostname in hostnames:
+ hostname_ips = []
+ for rdtype in ['A', 'AAAA']:
+ try:
+ answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
+ except dns.exception.Timeout:
+ logInfo('Hostname %s timedout on resolve' % hostname)
+ break
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ continue
+ except dns.exception.DNSException as dnsexception:
+ logInfo('%s' % dnsexception)
+ continue
+ for rdata in answer:
+ hostname_ips.append(rdata.to_text())
+ networks.extend(hostname_ips)
+ return set(networks)
+
+def whitelistUpdate():
+ global lock
+ global quit_now
+ global WHITELIST
+ while not quit_now:
+ start_time = time.time()
+ list = r.hgetall('NETFILTER_WHITELIST')
+ new_whitelist = []
+ if list:
+ new_whitelist = genNetworkList(list)
+ with lock:
+ if Counter(new_whitelist) != Counter(WHITELIST):
+ WHITELIST = new_whitelist
+ logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
+ time.sleep(60.0 - ((time.time() - start_time) % 60.0))
+
+def blacklistUpdate():
+ global quit_now
+ global BLACKLIST
+ while not quit_now:
+ start_time = time.time()
+ list = r.hgetall('NETFILTER_BLACKLIST')
+ new_blacklist = []
+ if list:
+ new_blacklist = genNetworkList(list)
+ if Counter(new_blacklist) != Counter(BLACKLIST):
+ addban = set(new_blacklist).difference(BLACKLIST)
+ delban = set(BLACKLIST).difference(new_blacklist)
+ BLACKLIST = new_blacklist
+ logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
+ if addban:
+ for net in addban:
+ permBan(net=net)
+ if delban:
+ for net in delban:
+ permBan(net=net, unban=True)
+ time.sleep(60.0 - ((time.time() - start_time) % 60.0))
+
+def initChain():
+ # Is called before threads start, no locking
+ print("Initializing mailcow netfilter chain")
+ # IPv4
+ if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
+ iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
+ for c in ['FORWARD', 'INPUT']:
+ chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
+ rule = iptc.Rule()
+ rule.src = '0.0.0.0/0'
+ rule.dst = '0.0.0.0/0'
+ target = iptc.Target(rule, "MAILCOW")
+ rule.target = target
+ if rule not in chain.rules:
+ chain.insert_rule(rule)
+ # IPv6
+ if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
+ iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
+ for c in ['FORWARD', 'INPUT']:
+ chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
+ rule = iptc.Rule6()
+ rule.src = '::/0'
+ rule.dst = '::/0'
+ target = iptc.Target(rule, "MAILCOW")
+ rule.target = target
+ if rule not in chain.rules:
+ chain.insert_rule(rule)
+
+if __name__ == '__main__':
+
+ # In case a previous session was killed without cleanup
+ clear()
+ # Reinit MAILCOW chain
+ initChain()
+
+ watch_thread = Thread(target=watch)
+ watch_thread.daemon = True
+ watch_thread.start()
+
+ if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
+ try:
+ snat_ip = os.getenv('SNAT_TO_SOURCE')
+ snat_ipo = ipaddress.ip_address(snat_ip)
+ if type(snat_ipo) is ipaddress.IPv4Address:
+ snat4_thread = Thread(target=snat4,args=(snat_ip,))
+ snat4_thread.daemon = True
+ snat4_thread.start()
+ except ValueError:
+ print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
+
+ if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
+ try:
+ snat_ip = os.getenv('SNAT6_TO_SOURCE')
+ snat_ipo = ipaddress.ip_address(snat_ip)
+ if type(snat_ipo) is ipaddress.IPv6Address:
+ snat6_thread = Thread(target=snat6,args=(snat_ip,))
+ snat6_thread.daemon = True
+ snat6_thread.start()
+ except ValueError:
+ print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
+
+ autopurge_thread = Thread(target=autopurge)
+ autopurge_thread.daemon = True
+ autopurge_thread.start()
+
+ mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
+ mailcowchainwatch_thread.daemon = True
+ mailcowchainwatch_thread.start()
+
+ blacklistupdate_thread = Thread(target=blacklistUpdate)
+ blacklistupdate_thread.daemon = True
+ blacklistupdate_thread.start()
+
+ whitelistupdate_thread = Thread(target=whitelistUpdate)
+ whitelistupdate_thread.daemon = True
+ whitelistupdate_thread.start()
+
+ signal.signal(signal.SIGTERM, quit)
+ atexit.register(clear)
+
+ while not quit_now:
+ time.sleep(0.5)
+
+ sys.exit(exit_code)
diff --git a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf
index 558305ec..605ee3a9 100644
--- a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf
+++ b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf
@@ -23,12 +23,12 @@ destination d_redis_ui_log {
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
-destination d_redis_f2b_channel {
+destination d_redis_netfilter_channel {
redis(
host("`REDIS_SLAVEOF_IP`")
persist-name("redis2")
port(`REDIS_SLAVEOF_PORT`)
- command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
+ command("PUBLISH" "NETFILTER_CHANNEL" "$(sanitize $MESSAGE)")
);
};
filter f_mail { facility(mail); };
@@ -49,5 +49,5 @@ log {
destination(d_stdout);
filter(f_mail);
destination(d_redis_ui_log);
- destination(d_redis_f2b_channel);
+ destination(d_redis_netfilter_channel);
};
diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf
index a1ccd07e..418e68f5 100644
--- a/data/Dockerfiles/postfix/syslog-ng.conf
+++ b/data/Dockerfiles/postfix/syslog-ng.conf
@@ -23,12 +23,12 @@ destination d_redis_ui_log {
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
-destination d_redis_f2b_channel {
+destination d_redis_netfilter_channel {
redis(
host("redis-mailcow")
persist-name("redis2")
port(6379)
- command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
+ command("PUBLISH" "NETFILTER_CHANNEL" "$(sanitize $MESSAGE)")
);
};
filter f_mail { facility(mail); };
@@ -49,5 +49,5 @@ log {
destination(d_stdout);
filter(f_mail);
destination(d_redis_ui_log);
- destination(d_redis_f2b_channel);
+ destination(d_redis_netfilter_channel);
};
diff --git a/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf b/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf
index 9b460bd3..6cddd167 100644
--- a/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf
+++ b/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf
@@ -25,18 +25,18 @@ destination d_redis_ui_log {
command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
-destination d_redis_f2b_channel {
+destination d_redis_netfilter_channel {
redis(
host("`REDIS_SLAVEOF_IP`")
persist-name("redis2")
port(`REDIS_SLAVEOF_PORT`)
- command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
+ command("PUBLISH" "NETFILTER_CHANNEL" "$(sanitize $MESSAGE)")
);
};
log {
source(s_sogo);
destination(d_redis_ui_log);
- destination(d_redis_f2b_channel);
+ destination(d_redis_netfilter_channel);
};
log {
source(s_sogo);
diff --git a/data/Dockerfiles/sogo/syslog-ng.conf b/data/Dockerfiles/sogo/syslog-ng.conf
index 889a3f32..3e3d1c8b 100644
--- a/data/Dockerfiles/sogo/syslog-ng.conf
+++ b/data/Dockerfiles/sogo/syslog-ng.conf
@@ -25,18 +25,18 @@ destination d_redis_ui_log {
command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
-destination d_redis_f2b_channel {
+destination d_redis_netfilter_channel {
redis(
host("redis-mailcow")
persist-name("redis2")
port(6379)
- command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
+ command("PUBLISH" "NETFILTER_CHANNEL" "$(sanitize $MESSAGE)")
);
};
log {
source(s_sogo);
destination(d_redis_ui_log);
- destination(d_redis_f2b_channel);
+ destination(d_redis_netfilter_channel);
};
log {
source(s_sogo);
diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh
index 231d0ecd..d44c2bdf 100755
--- a/data/Dockerfiles/watchdog/watchdog.sh
+++ b/data/Dockerfiles/watchdog/watchdog.sh
@@ -48,7 +48,7 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
sleep 2
done
-${REDIS_CMDLINE} DEL F2B_RES > /dev/null
+${REDIS_CMDLINE} DEL NETFILTER_RES > /dev/null
# Common functions
get_ipv6(){
@@ -116,7 +116,7 @@ function mail_error() {
fi
WATCHDOG_NOTIFY_EMAIL=$(echo "${WATCHDOG_NOTIFY_EMAIL}" | sed 's/"//;s|"$||')
# Some exceptions for subject and body formats
- if [[ ${1} == "fail2ban" ]]; then
+ if [[ ${1} == "netfilter" ]]; then
SUBJECT="${BODY}"
BODY="Please see netfilter-mailcow for more details and triggered rules."
else
@@ -607,29 +607,29 @@ mailq_checks() {
return 1
}
-fail2ban_checks() {
+netfilter_checks() {
err_count=0
diff_c=0
- THRESHOLD=${FAIL2BAN_THRESHOLD}
- F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
- F2B_RES=
+ THRESHOLD=${NETFILTER_THRESHOLD}
+ NETFILTER_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS NETFILTER_ACTIVE_BANS))
+ NETFILTER_RES=
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
- F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]})
- F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
- array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV
- if [[ ! -z "${F2B_RES}" ]]; then
+ NETFILTER_LOG_STATUS_PREV=(${NETFILTER_LOG_STATUS[@]})
+ NETFILTER_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS NETFILTER_ACTIVE_BANS))
+ array_diff NETFILTER_RES NETFILTER_LOG_STATUS NETFILTER_LOG_STATUS_PREV
+ if [[ ! -z "${NETFILTER_RES}" ]]; then
err_count=$(( ${err_count} + 1 ))
- echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null
+ echo -n "${NETFILTER_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET NETFILTER_RES > /dev/null
if [ $? -ne 0 ]; then
- ${REDIS_CMDLINE} -x DEL F2B_RES
+ ${REDIS_CMDLINE} -x DEL NETFILTER_RES
fi
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
- progress "Fail2ban" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
+ progress "Netfilter" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
@@ -944,14 +944,14 @@ BACKGROUND_TASKS+=(${PID})
(
while true; do
- if ! fail2ban_checks; then
- log_msg "Fail2ban hit error limit"
- echo fail2ban > /tmp/com_pipe
+ if ! netfilter_checks; then
+ log_msg "Netfilter hit error limit"
+ echo netfilter > /tmp/com_pipe
fi
done
) &
PID=$!
-echo "Spawned fail2ban_checks with PID ${PID}"
+echo "Spawned netfilter_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
@@ -1056,15 +1056,15 @@ while true; do
log_msg "acme-mailcow did not complete successfully"
# Define $2 to override message text, else print service was restarted at ...
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
- elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
- F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
- if [[ ! -z "${F2B_RES}" ]]; then
- ${REDIS_CMDLINE} DEL F2B_RES > /dev/null
+ elif [[ ${com_pipe_answer} == "netfilter" ]]; then
+ NETFILTER_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET NETFILTER_RES 2> /dev/null))
+ if [[ ! -z "${NETFILTER_RES}" ]]; then
+ ${REDIS_CMDLINE} DEL NETFILTER_RES > /dev/null
host=
- for host in "${F2B_RES[@]}"; do
+ for host in "${NETFILTER_RES[@]}"; do
log_msg "Banned ${host}"
- rm /tmp/fail2ban 2> /dev/null
- timeout 2s whois "${host}" > /tmp/fail2ban
+ rm /tmp/netfilter 2> /dev/null
+ timeout 2s whois "${host}" > /tmp/netfilter
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}"
done
fi
diff --git a/data/web/_rspamderror.php b/data/web/_rspamderror.php
index 6bdfb349..c3631493 100644
--- a/data/web/_rspamderror.php
+++ b/data/web/_rspamderror.php
@@ -14,4 +14,4 @@ catch (Exception $e) {
header('Content-Type: application/json');
echo '{"error":"Unauthorized"}';
error_log("Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
-$redis->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
+$redis->publish("NETFILTER_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
diff --git a/data/web/admin.php b/data/web/admin.php
index cd3eb890..7eae5b59 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -96,7 +96,7 @@ $template_data = [
'domains' => $domains,
'all_domains' => $all_domains,
'mailboxes' => $mailboxes,
- 'f2b_data' => fail2ban('get'),
+ 'netfilter_data' => netfilter('get'),
'q_data' => quarantine('settings'),
'qn_data' => quota_notification('get'),
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml
index 6310aa58..3ac62087 100644
--- a/data/web/api/openapi.yaml
+++ b/data/web/api/openapi.yaml
@@ -1999,7 +1999,7 @@ paths:
- domain.tld
- domain2.tld
properties:
- items:
+ items:
type: array
items:
type: string
@@ -2993,7 +2993,7 @@ paths:
application/json:
schema:
type: array
- items:
+ items:
type: object
properties:
log:
@@ -3105,7 +3105,7 @@ paths:
type: string
type: object
summary: Update domain
- /api/v1/edit/fail2ban:
+ /api/v1/edit/netfilter:
post:
responses:
"401":
@@ -3132,11 +3132,11 @@ paths:
description: OK
headers: {}
tags:
- - Fail2Ban
+ - Netfilter
description: >-
- Using this endpoint you can edit the Fail2Ban config and black or
+ Using this endpoint you can edit the Netfilter config and black or
whitelist new ips.
- operationId: Edit Fail2Ban
+ operationId: Edit Netfilter
requestBody:
content:
application/json:
@@ -3153,7 +3153,7 @@ paths:
items: none
properties:
attr:
- description: array containing the fail2ban settings
+ description: array containing the netfilter settings
properties:
backlist:
description: the backlisted ips or hostnames separated by comma
@@ -3182,7 +3182,7 @@ paths:
items:
description: has to be none
type: object
- summary: Edit Fail2Ban
+ summary: Edit Netfilter
/api/v1/edit/mailbox:
post:
responses:
@@ -4069,7 +4069,7 @@ paths:
description: You can list all domains existing in system.
operationId: Get domains
summary: Get domains
- /api/v1/get/fail2ban:
+ /api/v1/get/netfilter:
get:
responses:
"401":
@@ -4095,10 +4095,10 @@ paths:
description: OK
headers: {}
tags:
- - Fail2Ban
- description: Gets the current Fail2Ban configuration.
- operationId: Get Fail2Ban Config
- summary: Get Fail2Ban Config
+ - Netfilter
+ description: Gets the current Netfilter configuration.
+ operationId: Get Netfilter Config
+ summary: Get Netfilter Config
/api/v1/get/fwdhost/all:
get:
responses:
@@ -4522,7 +4522,7 @@ paths:
hpnow: "1"
hptotal: "1"
lvl: "100"
- service: Fail2ban
+ service: Netfilter
time: "1569938958"
- hpdiff: "0"
hpnow: "5"
@@ -5580,8 +5580,8 @@ tags:
description: Manage the postfix mail queue
- name: Quarantine
description: Check what emails went to quarantine
- - name: Fail2Ban
- description: Manage the Netfilter fail2ban options
+ - name: Netfilter
+ description: Manage the Netfilter netfilter options
- name: DKIM
description: Manage DKIM keys
- name: Domain admin
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 3bab56bb..5e3eeb8f 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -1,2405 +1,2405 @@
-1) {
- $bits=(int)($ar[1]);
- }
- else {
- $bits = $iplen * 8;
- }
- for ($c=0; $bits>0; $c++) {
- $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
- if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0) {
- continue 2;
- }
- $bits-=8;
- }
- return true;
- }
- return false;
-}
-function hash_password($password) {
- // default_pass_scheme is determined in vars.inc.php (or corresponding local file)
- // in case default pass scheme is not defined, falling back to BLF-CRYPT.
- global $default_pass_scheme;
- $pw_hash = NULL;
- // support pre-hashed passwords
- if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
- $pw_hash = $password;
- }
- else {
- switch (strtoupper($default_pass_scheme)) {
- case "SSHA":
- $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
- $pw_hash = "{SSHA}".base64_encode(hash('sha1', $password . $salt_str, true) . $salt_str);
- break;
- case "SSHA256":
- $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
- $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
- break;
- case "SSHA512":
- $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
- $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
- break;
- case "BLF-CRYPT":
- default:
- $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
- break;
- }
- }
- return $pw_hash;
-}
-function password_complexity($_action, $_data = null) {
- global $redis;
- global $lang;
- switch ($_action) {
- case 'edit':
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $is_now = password_complexity('get');
- if (!empty($is_now)) {
- $length = (isset($_data['length']) && intval($_data['length']) >= 3) ? intval($_data['length']) : $is_now['length'];
- $chars = (isset($_data['chars'])) ? intval($_data['chars']) : $is_now['chars'];
- $lowerupper = (isset($_data['lowerupper'])) ? intval($_data['lowerupper']) : $is_now['lowerupper'];
- $special_chars = (isset($_data['special_chars'])) ? intval($_data['special_chars']) : $is_now['special_chars'];
- $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
- }
- try {
- $redis->hMSet('PASSWD_POLICY', [
- 'length' => $length,
- 'chars' => $chars,
- 'special_chars' => $special_chars,
- 'lowerupper' => $lowerupper,
- 'numbers' => $numbers
- ]);
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_action, $_data),
- 'msg' => 'password_policy_saved'
- );
- break;
- case 'get':
- try {
- $length = $redis->hGet('PASSWD_POLICY', 'length');
- $chars = $redis->hGet('PASSWD_POLICY', 'chars');
- $special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
- $lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
- $numbers = $redis->hGet('PASSWD_POLICY', 'numbers');
- return array(
- 'length' => $length,
- 'chars' => $chars,
- 'special_chars' => $special_chars,
- 'lowerupper' => $lowerupper,
- 'numbers' => $numbers
- );
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- return false;
- break;
- case 'html':
- $policies = password_complexity('get');
- foreach ($policies as $name => $value) {
- if ($value != 0) {
- $policy_text[] = sprintf($lang['admin']["password_policy_$name"], $value);
- }
- }
- return '
- ' . implode(' - ', (array)$policy_text) . '
';
- break;
- }
-}
-function password_check($password1, $password2) {
- $password_complexity = password_complexity('get');
-
- if (empty($password1) || empty($password2)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_type),
- 'msg' => 'password_complexity'
- );
- return false;
- }
-
- if ($password1 != $password2) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_type),
- 'msg' => 'password_mismatch'
- );
- return false;
- }
-
- $given_password['length'] = strlen($password1);
- $given_password['special_chars'] = preg_match('/[^a-zA-Z\d]/', $password1);
- $given_password['chars'] = preg_match('/[a-zA-Z]/',$password1);
- $given_password['numbers'] = preg_match('/\d/', $password1);
- $lower = strlen(preg_replace("/[^a-z]/", '', $password1));
- $upper = strlen(preg_replace("/[^A-Z]/", '', $password1));
- $given_password['lowerupper'] = ($lower > 0 && $upper > 0) ? true : false;
-
- if (
- ($given_password['length'] < $password_complexity['length']) ||
- ($password_complexity['special_chars'] == 1 && (intval($given_password['special_chars']) != $password_complexity['special_chars'])) ||
- ($password_complexity['chars'] == 1 && (intval($given_password['chars']) != $password_complexity['chars'])) ||
- ($password_complexity['numbers'] == 1 && (intval($given_password['numbers']) != $password_complexity['numbers'])) ||
- ($password_complexity['lowerupper'] == 1 && (intval($given_password['lowerupper']) != $password_complexity['lowerupper']))
- ) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_type),
- 'msg' => 'password_complexity'
- );
- return false;
- }
-
- return true;
-}
-function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
- global $pdo;
- global $redis;
- $sasl_limit_days = intval($sasl_limit_days);
- switch ($action) {
- case 'get':
- if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
- $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service`, `app_password`, MAX(`app_passwd`.`name`) as `app_password_name` FROM `sasl_log`
- LEFT OUTER JOIN `app_passwd` on `sasl_log`.`app_password` = `app_passwd`.`id`
- WHERE `username` = :username
- AND HOUR(TIMEDIFF(NOW(), `datetime`)) < :sasl_limit_days
- GROUP BY `real_rip`, `service`, `app_password`
- ORDER BY `datetime` DESC;');
- $stmt->execute(array(':username' => $username, ':sasl_limit_days' => ($sasl_limit_days * 24)));
- $sasl = $stmt->fetchAll(PDO::FETCH_ASSOC);
- foreach ($sasl as $k => $v) {
- if (!filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
- $sasl[$k]['real_rip'] = 'Web/EAS/Internal (' . $sasl[$k]['real_rip'] . ')';
- }
- elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
- try {
- $sasl[$k]['location'] = $redis->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']);
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- if (!$sasl[$k]['location']) {
- $curl = curl_init();
- curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']);
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
- curl_setopt($curl, CURLOPT_TIMEOUT, 5);
- $ip_data = curl_exec($curl);
- if (!curl_errno($curl)) {
- $ip_data_array = json_decode($ip_data, true);
- if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) {
- $sasl[$k]['location'] = $ip_data_array['location']['shortcountry'];
- try {
- $redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']);
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- curl_close($curl);
- return false;
- }
- }
- }
- curl_close($curl);
- }
- }
- }
- }
- else {
- $sasl = array();
- }
- if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
- $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
- WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
- AND JSON_EXTRACT(`call`, "$[1]") = :username
- AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET :offset');
- $stmt->execute(array(
- ':username' => $username,
- ':offset' => $ui_offset
- ));
- $ui = $stmt->fetch(PDO::FETCH_ASSOC);
- }
- else {
- $ui = array();
- }
-
- return array('ui' => $ui, 'sasl' => $sasl);
- break;
- case 'reset':
- if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
- $stmt = $pdo->prepare('DELETE FROM `sasl_log`
- WHERE `username` = :username');
- $stmt->execute(array(':username' => $username));
- }
- if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
- $stmt = $pdo->prepare('DELETE FROM `logs`
- WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
- AND JSON_EXTRACT(`call`, "$[1]") = :username
- AND `type` = "success"');
- $stmt->execute(array(':username' => $username));
- }
- return true;
- break;
- }
-
-}
-function flush_memcached() {
- try {
- $m = new Memcached();
- $m->addServer('memcached', 11211);
- $m->flush();
- }
- catch ( Exception $e ) {
- // Dunno
- }
-}
-function sys_mail($_data) {
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $excludes = $_data['mass_exclude'];
- $includes = $_data['mass_include'];
- $mailboxes = array();
- $mass_from = $_data['mass_from'];
- $mass_text = $_data['mass_text'];
- $mass_html = $_data['mass_html'];
- $mass_subject = $_data['mass_subject'];
- if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'from_invalid'
- );
- return false;
- }
- if (empty($mass_subject)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'subject_empty'
- );
- return false;
- }
- if (empty($mass_text)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'text_empty'
- );
- return false;
- }
- $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
- foreach ($domains as $domain) {
- foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
- $mailboxes[] = $mailbox;
- }
- }
- if (!empty($includes)) {
- $rcpts = array_intersect($mailboxes, $includes);
- }
- elseif (!empty($excludes)) {
- $rcpts = array_diff($mailboxes, $excludes);
- }
- else {
- $rcpts = $mailboxes;
- }
- if (!empty($rcpts)) {
- ini_set('max_execution_time', 0);
- ini_set('max_input_time', 0);
- $mail = new PHPMailer;
- $mail->Timeout = 10;
- $mail->SMTPOptions = array(
- 'ssl' => array(
- 'verify_peer' => false,
- 'verify_peer_name' => false,
- 'allow_self_signed' => true
- )
- );
- $mail->isSMTP();
- $mail->Host = 'dovecot-mailcow';
- $mail->SMTPAuth = false;
- $mail->Port = 24;
- $mail->setFrom($mass_from);
- $mail->Subject = $mass_subject;
- $mail->CharSet ="UTF-8";
- if (!empty($mass_html)) {
- $mail->Body = $mass_html;
- $mail->AltBody = $mass_text;
- }
- else {
- $mail->Body = $mass_text;
- }
- $mail->XMailer = 'MooMassMail';
- foreach ($rcpts as $rcpt) {
- $mail->AddAddress($rcpt);
- if (!$mail->send()) {
- $_SESSION['return'][] = array(
- 'type' => 'warning',
- 'log' => array(__FUNCTION__),
- 'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
- );
- }
- $mail->ClearAllRecipients();
- }
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__),
- 'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
- );
-}
-function logger($_data = false) {
- /*
- logger() will be called as last function
- To manually log a message, logger needs to be called like below.
-
- logger(array(
- 'return' => array(
- array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => $err
- )
- )
- ));
-
- These messages will not be printed as alert box.
- To do so, push them to $_SESSION['return'] and do not call logger as they will be included automatically:
-
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => $err
- );
- */
- global $pdo;
- if (!$_data) {
- $_data = $_SESSION;
- }
- if (!empty($_data['return'])) {
- $task = substr(strtoupper(md5(uniqid(rand(), true))), 0, 6);
- foreach ($_data['return'] as $return) {
- $type = $return['type'];
- $msg = null;
- if (isset($return['msg'])) {
- $msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
- }
- $call = null;
- if (isset($return['log'])) {
- $call = json_encode($return['log'], JSON_UNESCAPED_UNICODE);
- }
- if (!empty($_SESSION["dual-login"]["username"])) {
- $user = $_SESSION["dual-login"]["username"] . ' => ' . $_SESSION['mailcow_cc_username'];
- $role = $_SESSION["dual-login"]["role"] . ' => ' . $_SESSION['mailcow_cc_role'];
- }
- elseif (!empty($_SESSION['mailcow_cc_username'])) {
- $user = $_SESSION['mailcow_cc_username'];
- $role = $_SESSION['mailcow_cc_role'];
- }
- else {
- $user = 'unauthenticated';
- $role = 'unauthenticated';
- }
- // We cannot log when logs is missing...
- try {
- $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
- (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
- $stmt->execute(array(
- ':type' => $type,
- ':task' => $task,
- ':call' => $call,
- ':msg' => $msg,
- ':user' => $user,
- ':role' => $role,
- ':remote' => get_remote_ip()
- ));
- }
- catch (Exception $e) {
- // Do nothing
- }
- }
- }
- else {
- return true;
- }
-}
-function hasDomainAccess($username, $role, $domain) {
- global $pdo;
- if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
- return false;
- }
- if (empty($domain) || !is_valid_domain_name($domain)) {
- return false;
- }
- if ($role != 'admin' && $role != 'domainadmin') {
- return false;
- }
- if ($role == 'admin') {
- $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
- WHERE `domain` = :domain");
- $stmt->execute(array(':domain' => $domain));
- $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
- $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
- WHERE `alias_domain` = :domain");
- $stmt->execute(array(':domain' => $domain));
- $num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
- if ($num_results != 0) {
- return true;
- }
- }
- elseif ($role == 'domainadmin') {
- $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
- WHERE (
- `active`='1'
- AND `username` = :username
- AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
- )");
- $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
- $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
- if (!empty($num_results)) {
- return true;
- }
- }
- return false;
-}
-function hasMailboxObjectAccess($username, $role, $object) {
- global $pdo;
- if (empty($username) || empty($role) || empty($object)) {
- return false;
- }
- if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
- return false;
- }
- if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
- return false;
- }
- if ($username == $object) {
- return true;
- }
- $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
- $stmt->execute(array(':object' => $object));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
- return true;
- }
- return false;
-}
-// does also verify mailboxes as a mailbox is a alias == goto
-function hasAliasObjectAccess($username, $role, $object) {
- global $pdo;
- if (empty($username) || empty($role) || empty($object)) {
- return false;
- }
- if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
- return false;
- }
- if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
- return false;
- }
- $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
- $stmt->execute(array(':object' => $object));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
- return true;
- }
- return false;
-}
-function pem_to_der($pem_key) {
- // Need to remove BEGIN/END PUBLIC KEY
- $lines = explode("\n", trim($pem_key));
- unset($lines[count($lines)-1]);
- unset($lines[0]);
- return base64_decode(implode('', $lines));
-}
-function expand_ipv6($ip) {
- $hex = unpack("H*hex", inet_pton($ip));
- $ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
- return $ip;
-}
-function generate_tlsa_digest($hostname, $port, $starttls = null) {
- if (!is_valid_domain_name($hostname)) {
- return "Not a valid hostname";
- }
- if (empty($starttls)) {
- $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true)));
- $stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
- if (!$stream) {
- $error_msg = isset($error_msg) ? $error_msg : '-';
- return $error_nr . ': ' . $error_msg;
- }
- }
- else {
- $stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
- if (!$stream) {
- return $error_nr . ': ' . $error_msg;
- }
- $banner = fread($stream, 512 );
- if (preg_match("/^220/i", $banner)) { // SMTP
- fwrite($stream,"HELO tlsa.generator.local\r\n");
- fread($stream, 512);
- fwrite($stream,"STARTTLS\r\n");
- fread($stream, 512);
- }
- elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP
- fwrite($stream,"A1 STARTTLS\r\n");
- fread($stream, 512);
- }
- elseif (preg_match("/^\+OK/", $banner)) { // POP3
- fwrite($stream,"STLS\r\n");
- fread($stream, 512);
- }
- elseif (preg_match("/^OK/m", $banner)) { // Sieve
- fwrite($stream,"STARTTLS\r\n");
- fread($stream, 512);
- }
- else {
- return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"';
- }
- // Upgrade connection
- stream_set_blocking($stream, true);
- stream_context_set_option($stream, 'ssl', 'capture_peer_cert', true);
- stream_context_set_option($stream, 'ssl', 'verify_peer', false);
- stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
- stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
- stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
- stream_set_blocking($stream, false);
- }
- $params = stream_context_get_params($stream);
- if (!empty($params['options']['ssl']['peer_certificate'])) {
- $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']);
- // We cannot get ['rsa']['n'], the binary data would contain BEGIN/END PUBLIC KEY
- $key_data = openssl_pkey_get_details($key_resource)['key'];
- return '3 1 1 ' . openssl_digest(pem_to_der($key_data), 'sha256');
- }
- else {
- return 'Error: Cannot read peer certificate';
- }
-}
-function alertbox_log_parser($_data) {
- global $lang;
- if (isset($_data['return'])) {
- foreach ($_data['return'] as $return) {
- // Get type
- $type = $return['type'];
- // If a lang[type][msg] string exists, use it as message
- if (isset($return['type']) && isset($return['msg']) && !is_array($return['msg'])) {
- if (isset($lang[$return['type']][$return['msg']])) {
- $msg = $lang[$return['type']][$return['msg']];
- }
- else {
- $msg = $return['msg'];
- }
- }
- // If msg is an array, use first element as language string and run printf on it with remaining array elements
- elseif (is_array($return['msg'])) {
- $msg = array_shift($return['msg']);
- $msg = vsprintf(
- $lang[$return['type']][$msg],
- $return['msg']
- );
- }
- else {
- $msg = '-';
- }
- $log_array[] = array('msg' => $msg, 'type' => json_encode($type));
- }
- if (!empty($log_array)) {
- return $log_array;
- }
- }
- return false;
-}
-function verify_salted_hash($hash, $password, $algo, $salt_length) {
- // Decode hash
- $dhash = base64_decode($hash);
- // Get first n bytes of binary which equals a SSHA hash
- $ohash = substr($dhash, 0, $salt_length);
- // Remove SSHA hash from decoded hash to get original salt string
- $osalt = str_replace($ohash, '', $dhash);
- // Check single salted SSHA hash against extracted hash
- if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
- return true;
- }
- return false;
-}
-function verify_hash($hash, $password) {
- if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
- $scheme = strtoupper($hash_array[1]);
- $hash = $hash_array[2];
- switch ($scheme) {
- case "ARGON2I":
- case "ARGON2ID":
- case "BLF-CRYPT":
- case "CRYPT":
- case "DES-CRYPT":
- case "MD5-CRYPT":
- case "MD5":
- case "SHA256-CRYPT":
- case "SHA512-CRYPT":
- return password_verify($password, $hash);
-
- case "CLEAR":
- case "CLEARTEXT":
- case "PLAIN":
- return $password == $hash;
-
- case "LDAP-MD5":
- $hash = base64_decode($hash);
- return hash_equals(hash('md5', $password, true), $hash);
-
- case "PBKDF2":
- $components = explode('$', $hash);
- $salt = $components[2];
- $rounds = $components[3];
- $hash = $components[4];
- return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
-
- case "PLAIN-MD4":
- return hash_equals(hash('md4', $password), $hash);
-
- case "PLAIN-MD5":
- return md5($password) == $hash;
-
- case "PLAIN-TRUNC":
- $components = explode('-', $hash);
- if (count($components) > 1) {
- $trunc_len = $components[0];
- $trunc_password = $components[1];
-
- return substr($password, 0, $trunc_len) == $trunc_password;
- } else {
- return $password == $hash;
- }
-
- case "SHA":
- case "SHA1":
- case "SHA256":
- case "SHA512":
- // SHA is an alias for SHA1
- $scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
- $hash = base64_decode($hash);
- return hash_equals(hash($scheme, $password, true), $hash);
-
- case "SMD5":
- return verify_salted_hash($hash, $password, 'md5', 16);
-
- case "SSHA":
- return verify_salted_hash($hash, $password, 'sha1', 20);
-
- case "SSHA256":
- return verify_salted_hash($hash, $password, 'sha256', 32);
-
- case "SSHA512":
- return verify_salted_hash($hash, $password, 'sha512', 64);
-
- default:
- return false;
- }
- }
- return false;
-}
-function check_login($user, $pass, $app_passwd_data = false) {
- global $pdo;
- global $redis;
- global $imap_server;
-
- if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'malformed_username'
- );
- return false;
- }
-
- // Validate admin
- $user = strtolower(trim($user));
- $stmt = $pdo->prepare("SELECT `password` FROM `admin`
- WHERE `superadmin` = '1'
- AND `active` = '1'
- AND `username` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- foreach ($rows as $row) {
- // verify password
- if (verify_hash($row['password'], $pass)) {
- // check for tfa authenticators
- $authenticators = get_tfa($user);
- if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
- // active tfa authenticators found, set pending user login
- $_SESSION['pending_mailcow_cc_username'] = $user;
- $_SESSION['pending_mailcow_cc_role'] = "admin";
- $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
- unset($_SESSION['ldelay']);
- $_SESSION['return'][] = array(
- 'type' => 'info',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'awaiting_tfa_confirmation'
- );
- return "pending";
- } else {
- unset($_SESSION['ldelay']);
- // Reactivate TFA if it was set to "deactivate TFA for next login"
- $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
- $stmt->execute(array(':user' => $user));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => array('logged_in_as', $user)
- );
- return "admin";
- }
- }
- }
-
- // Validate domain admin
- $stmt = $pdo->prepare("SELECT `password` FROM `admin`
- WHERE `superadmin` = '0'
- AND `active`='1'
- AND `username` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- foreach ($rows as $row) {
- // verify password
- if (verify_hash($row['password'], $pass) !== false) {
- // check for tfa authenticators
- $authenticators = get_tfa($user);
- if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
- $_SESSION['pending_mailcow_cc_username'] = $user;
- $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
- $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
- unset($_SESSION['ldelay']);
- $_SESSION['return'][] = array(
- 'type' => 'info',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'awaiting_tfa_confirmation'
- );
- return "pending";
- }
- else {
- unset($_SESSION['ldelay']);
- // Reactivate TFA if it was set to "deactivate TFA for next login"
- $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
- $stmt->execute(array(':user' => $user));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => array('logged_in_as', $user)
- );
- return "domainadmin";
- }
- }
- }
-
- // Validate mailbox user
- $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
- INNER JOIN domain on mailbox.domain = domain.domain
- WHERE `kind` NOT REGEXP 'location|thing|group'
- AND `mailbox`.`active`='1'
- AND `domain`.`active`='1'
- AND `username` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- if ($app_passwd_data['eas'] === true) {
- $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
- INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
- INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
- WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
- AND `mailbox`.`active` = '1'
- AND `domain`.`active` = '1'
- AND `app_passwd`.`active` = '1'
- AND `app_passwd`.`eas_access` = '1'
- AND `app_passwd`.`mailbox` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
- }
- elseif ($app_passwd_data['dav'] === true) {
- $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
- INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
- INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
- WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
- AND `mailbox`.`active` = '1'
- AND `domain`.`active` = '1'
- AND `app_passwd`.`active` = '1'
- AND `app_passwd`.`dav_access` = '1'
- AND `app_passwd`.`mailbox` = :user");
- $stmt->execute(array(':user' => $user));
- $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
- }
- foreach ($rows as $row) {
- // verify password
- if (verify_hash($row['password'], $pass) !== false) {
- if (!array_key_exists("app_passwd_id", $row)){
- // password is not a app password
- // check for tfa authenticators
- $authenticators = get_tfa($user);
- if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&
- $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {
- // authenticators found, init TFA flow
- $_SESSION['pending_mailcow_cc_username'] = $user;
- $_SESSION['pending_mailcow_cc_role'] = "user";
- $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
- unset($_SESSION['ldelay']);
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => array('logged_in_as', $user)
- );
- return "pending";
- } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
- // no authenticators found, login successfull
- // Reactivate TFA if it was set to "deactivate TFA for next login"
- $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
- $stmt->execute(array(':user' => $user));
-
- unset($_SESSION['ldelay']);
- return "user";
- }
- } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
- // password is a app password
- $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
- $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
- $stmt->execute(array(
- ':service' => $service,
- ':app_id' => $row['app_passwd_id'],
- ':username' => $user,
- ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
- ));
-
- unset($_SESSION['ldelay']);
- return "user";
- }
- }
- }
-
- if (!isset($_SESSION['ldelay'])) {
- $_SESSION['ldelay'] = "0";
- $redis->publish("F2B_CHANNEL", "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;
- $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- }
-
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $user, '*'),
- 'msg' => 'login_failed'
- );
-
- sleep($_SESSION['ldelay']);
- return false;
-}
-function formatBytes($size, $precision = 2) {
- if(!is_numeric($size)) {
- return "0";
- }
- $base = log($size, 1024);
- $suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
- if ($size == "0") {
- return "0";
- }
- return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
-}
-function update_sogo_static_view() {
- if (getenv('SKIP_SOGO') == "y") {
- return true;
- }
- global $pdo;
- global $lang;
- $stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
- WHERE TABLE_NAME = 'sogo_view'");
- $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
- if ($num_results != 0) {
- $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
- SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
- $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
- }
- flush_memcached();
-}
-function edit_user_account($_data) {
- global $lang;
- global $pdo;
- $_data_log = $_data;
- !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
- !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
- !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
- $username = $_SESSION['mailcow_cc_username'];
- $role = $_SESSION['mailcow_cc_role'];
- $password_old = $_data['user_old_pass'];
- if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
- WHERE `kind` NOT REGEXP 'location|thing|group'
- AND `username` = :user");
- $stmt->execute(array(':user' => $username));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (!verify_hash($row['password'], $password_old)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
- if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
- $password_new = $_data['user_new_pass'];
- $password_new2 = $_data['user_new_pass2'];
- if (password_check($password_new, $password_new2) !== true) {
- return false;
- }
- $password_hashed = hash_password($password_new);
- $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed,
- `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0'),
- `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
- WHERE `username` = :username");
- $stmt->execute(array(
- ':password_hashed' => $password_hashed,
- ':username' => $username
- ));
- }
- update_sogo_static_view();
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('mailbox_modified', htmlspecialchars($username))
- );
-}
-function user_get_alias_details($username) {
- global $pdo;
- global $lang;
- $data['direct_aliases'] = array();
- $data['shared_aliases'] = array();
- if ($_SESSION['mailcow_cc_role'] == "user") {
- $username = $_SESSION['mailcow_cc_username'];
- }
- if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
- return false;
- }
- if (!hasMailboxObjectAccess($username, $_SESSION['mailcow_cc_role'], $username)) {
- return false;
- }
- $data['address'] = $username;
- $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
- WHERE `goto` REGEXP :username_goto
- AND `address` NOT LIKE '@%'
- AND `goto` != :username_goto2
- AND `address` != :username_address");
- $stmt->execute(array(
- ':username_goto' => '(^|,)'.$username.'($|,)',
- ':username_goto2' => $username,
- ':username_address' => $username
- ));
- $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while ($row = array_shift($run)) {
- $data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
- //$data['shared_aliases'][] = $row['shared_aliases'];
- }
-
- $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
- WHERE `goto` = :username_goto
- AND `address` NOT LIKE '@%'
- AND `address` != :username_address");
- $stmt->execute(
- array(
- ':username_goto' => $username,
- ':username_address' => $username
- ));
- $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while ($row = array_shift($run)) {
- $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
- }
- $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
- LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
- WHERE `username` = :username ;");
- $stmt->execute(array(':username' => $username));
- $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while ($row = array_shift($run)) {
- if (empty($row['ad_alias'])) {
- continue;
- }
- $data['direct_aliases'][$row['ad_alias']]['public_comment'] = $lang['add']['alias_domain'];
- $data['alias_domains'][] = $row['alias_domain'];
- }
- $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
- $stmt->execute(array(':username' => $username));
- $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while ($row = array_shift($run)) {
- $data['aliases_also_send_as'] = $row['send_as'];
- }
- $stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ''), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
- $stmt->execute(array(':username' => $username));
- $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while ($row = array_shift($run)) {
- $data['aliases_send_as_all'] = $row['send_as'];
- }
- $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '') as `address` FROM `alias` WHERE `goto` REGEXP :username AND `address` LIKE '@%';");
- $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
- $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while ($row = array_shift($run)) {
- $data['is_catch_all'] = $row['address'];
- }
- return $data;
-}
-function is_valid_domain_name($domain_name) {
- if (empty($domain_name)) {
- return false;
- }
- $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
- return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
- && preg_match("/^.{1,253}$/", $domain_name)
- && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
-}
-function set_tfa($_data) {
- global $pdo;
- global $yubi;
- global $tfa;
- $_data_log = $_data;
- $access_denied = null;
- !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
- $username = $_SESSION['mailcow_cc_username'];
-
- // check for empty user and role
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
-
- // check admin confirm password
- if ($access_denied === null) {
- $stmt = $pdo->prepare("SELECT `password` FROM `admin`
- WHERE `username` = :username");
- $stmt->execute(array(':username' => $username));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($row) {
- if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
- else $access_denied = false;
- }
- }
-
- // check mailbox confirm password
- if ($access_denied === null) {
- $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
- WHERE `username` = :username");
- $stmt->execute(array(':username' => $username));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($row) {
- if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
- else $access_denied = false;
- }
- }
-
- // set access_denied error
- if ($access_denied){
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
-
- switch ($_data["tfa_method"]) {
- case "yubi_otp":
- $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
- $yubico_id = $_data['yubico_id'];
- $yubico_key = $_data['yubico_key'];
- $yubi = new Auth_Yubico($yubico_id, $yubico_key);
- if (!$yubi) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
- if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'tfa_token_invalid'
- );
- return false;
- }
- $yauth = $yubi->verify($_data["otp_token"]);
- if (PEAR::isError($yauth)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('yotp_verification_failed', $yauth->getMessage())
- );
- return false;
- }
- try {
- // We could also do a modhex translation here
- $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
- $stmt = $pdo->prepare("DELETE FROM `tfa`
- WHERE `username` = :username
- AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
- $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
- $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
- (:key_id, :username, 'yubi_otp', '1', :secret)");
- $stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
- }
- catch (PDOException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('mysql_error', $e)
- );
- return false;
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($username))
- );
- break;
- case "totp":
- $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
- if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
- //$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
- //$stmt->execute(array(':username' => $username));
- $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
- $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', $username)
- );
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'totp_verification_failed'
- );
- }
- break;
- case "webauthn":
- $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
-
- $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
- VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
- $stmt->execute(array(
- $username,
- $key_id,
- base64_encode($_data['registration']->credentialId),
- $_data['registration']->credentialPublicKey,
- $_data['registration']->certificate,
- 0
- ));
-
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', $username)
- );
- break;
- case "none":
- $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
- $stmt->execute(array(':username' => $username));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($username))
- );
- break;
- }
-}
-function fido2($_data) {
- global $pdo;
- $_data_log = $_data;
- // Not logging registration data, only actions
- // Silent errors for "get" requests
- switch ($_data["action"]) {
- case "register":
- $username = $_SESSION['mailcow_cc_username'];
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data["action"]),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
- $stmt->execute(array(
- $username,
- $_data['registration']->rpId,
- $_data['registration']->credentialPublicKey,
- $_data['registration']->certificateChain,
- $_data['registration']->certificate,
- $_data['registration']->certificateIssuer,
- $_data['registration']->certificateSubject,
- $_data['registration']->signatureCounter,
- $_data['registration']->AAGUID,
- $_data['registration']->credentialId)
- );
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data["action"]),
- 'msg' => array('object_modified', $username)
- );
- break;
- case "get_user_cids":
- // Used to exclude existing CredentialIds while registering
- $username = $_SESSION['mailcow_cc_username'];
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
- return false;
- }
- $stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
- $stmt->execute(array(':username' => $username));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $cids[] = $row['credentialId'];
- }
- return $cids;
- break;
- case "get_all_cids":
- // Only needed when using fido2 with username
- $stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $cids[] = $row['credentialId'];
- }
- return $cids;
- break;
- case "get_by_b64cid":
- if (!isset($_data['cid']) || empty($_data['cid'])) {
- return false;
- }
- $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE `credentialId` = :cid");
- $stmt->execute(array(':cid' => base64_decode($_data['cid'])));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
- return false;
- }
- $data['pub_key'] = $row['credentialPublicKey'];
- $data['username'] = $row['username'];
- $data['subject'] = $row['certificateSubject'];
- $data['cid'] = $row['cid'];
- return $data;
- break;
- case "get_friendly_names":
- $username = $_SESSION['mailcow_cc_username'];
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
- return false;
- }
- $stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
- $stmt->execute(array(':username' => $username));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $fns[] = array(
- "subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
- "fn" => $row['friendlyName'],
- "cid" => $row['cid']
- );
- }
- return $fns;
- break;
- case "unset_fido2_key":
- $username = $_SESSION['mailcow_cc_username'];
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data["action"]),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
- $stmt->execute(array(
- ':username' => $username,
- ':cid' => $_data['post_data']['unset_fido2_key']
- ));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($username))
- );
- break;
- case "edit_fn":
- $username = $_SESSION['mailcow_cc_username'];
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data["action"]),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
- $stmt->execute(array(
- ':username' => $username,
- ':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
- ':cid' => $_data['fido2_attrs']['fido2_cid']
- ));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($username))
- );
- break;
- }
-}
-function unset_tfa_key($_data) {
- // Can only unset own keys
- // Needs at least one key left
- global $pdo;
- global $lang;
- $_data_log = $_data;
- $access_denied = null;
- $id = intval($_data['unset_tfa_key']);
- $username = $_SESSION['mailcow_cc_username'];
-
- // check for empty user and role
- if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
-
- try {
- if (!is_numeric($id)) $access_denied = true;
-
- // set access_denied error
- if ($access_denied){
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
-
- // check if it's last key
- $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
- WHERE `username` = :username AND `active` = '1'");
- $stmt->execute(array(':username' => $username));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($row['keys'] == "1") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'last_key'
- );
- return false;
- }
-
- // delete key
- $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
- $stmt->execute(array(':username' => $username, ':id' => $id));
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('object_modified', $username)
- );
- }
- catch (PDOException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => array('mysql_error', $e)
- );
- return false;
- }
-}
-function get_tfa($username = null, $id = null) {
- global $pdo;
- if (isset($_SESSION['mailcow_cc_username'])) {
- $username = $_SESSION['mailcow_cc_username'];
- }
- elseif (empty($username)) {
- return false;
- }
-
- if (!isset($id)){
- // fetch all tfa methods - just get information about possible authenticators
- $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
- WHERE `username` = :username AND `active` = '1'");
- $stmt->execute(array(':username' => $username));
- $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
-
- // no tfa methods found
- if (count($results) == 0) {
- $data['name'] = 'none';
- $data['pretty'] = "-";
- $data['additional'] = array();
- return $data;
- }
-
- $data['additional'] = $results;
- return $data;
- } else {
- // fetch specific authenticator details by id
- $stmt = $pdo->prepare("SELECT * FROM `tfa`
- WHERE `username` = :username AND `id` = :id AND `active` = '1'");
- $stmt->execute(array(':username' => $username, ':id' => $id));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
-
- if (isset($row["authmech"])) {
- switch ($row["authmech"]) {
- case "yubi_otp":
- $data['name'] = "yubi_otp";
- $data['pretty'] = "Yubico OTP";
- $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");
- $stmt->execute(array(
- ':username' => $username,
- ':id' => $id
- ));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $data['additional'][] = $row;
- }
- return $data;
- break;
- // u2f - deprecated, should be removed
- case "u2f":
- $data['name'] = "u2f";
- $data['pretty'] = "Fido U2F";
- $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");
- $stmt->execute(array(
- ':username' => $username,
- ':id' => $id
- ));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $data['additional'][] = $row;
- }
- return $data;
- break;
- case "hotp":
- $data['name'] = "hotp";
- $data['pretty'] = "HMAC-based OTP";
- return $data;
- break;
- case "totp":
- $data['name'] = "totp";
- $data['pretty'] = "Time-based OTP";
- $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");
- $stmt->execute(array(
- ':username' => $username,
- ':id' => $id
- ));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $data['additional'][] = $row;
- }
- return $data;
- break;
- case "webauthn":
- $data['name'] = "webauthn";
- $data['pretty'] = "WebAuthn";
- $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");
- $stmt->execute(array(
- ':username' => $username,
- ':id' => $id
- ));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- while($row = array_shift($rows)) {
- $data['additional'][] = $row;
- }
- return $data;
- break;
- default:
- $data['name'] = 'none';
- $data['pretty'] = "-";
- return $data;
- break;
- }
- }
- else {
- $data['name'] = 'none';
- $data['pretty'] = "-";
- return $data;
- }
- }
-}
-function verify_tfa_login($username, $_data) {
- global $pdo;
- global $yubi;
- global $u2f;
- global $tfa;
- global $WebAuthn;
-
- if ($_data['tfa_method'] != 'u2f'){
-
- switch ($_data["tfa_method"]) {
- case "yubi_otp":
- if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('yotp_verification_failed', 'token length error')
- );
- return false;
- }
- $yubico_modhex_id = substr($_data['token'], 0, 12);
- $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
- WHERE `username` = :username
- AND `authmech` = 'yubi_otp'
- AND `active` = '1'
- AND `secret` LIKE :modhex");
- $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- $yubico_auth = explode(':', $row['secret']);
- $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
- $yauth = $yubi->verify($_data['token']);
- if (PEAR::isError($yauth)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('yotp_verification_failed', $yauth->getMessage())
- );
- return false;
- }
- else {
- $_SESSION['tfa_id'] = $row['id'];
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => 'verified_yotp_login'
- );
- return true;
- }
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('yotp_verification_failed', 'unknown')
- );
- return false;
- break;
- case "hotp":
- return false;
- break;
- case "totp":
- try {
- $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
- WHERE `username` = :username
- AND `authmech` = 'totp'
- AND `id` = :id
- AND `active`='1'");
- $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- foreach ($rows as $row) {
- if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
- $_SESSION['tfa_id'] = $row['id'];
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => 'verified_totp_login'
- );
- return true;
- }
- }
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => 'totp_verification_failed'
- );
- return false;
- }
- catch (PDOException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('mysql_error', $e)
- );
- return false;
- }
- break;
- case "webauthn":
- $tokenData = json_decode($_data['token']);
- $clientDataJSON = base64_decode($tokenData->clientDataJSON);
- $authenticatorData = base64_decode($tokenData->authenticatorData);
- $signature = base64_decode($tokenData->signature);
- $id = base64_decode($tokenData->id);
- $challenge = $_SESSION['challenge'];
-
- $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
- $stmt->execute(array(':id' => $_data['id']));
- $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
-
- if (empty($process_webauthn)){
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('webauthn_verification_failed', 'authenticator not found')
- );
- return false;
- }
-
- if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('webauthn_verification_failed', 'publicKey not found')
- );
- return false;
- }
-
- try {
- $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
- }
- catch (Throwable $ex) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('webauthn_verification_failed', $ex->getMessage())
- );
- return false;
- }
-
- $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
- $stmt->execute(array(':username' => $process_webauthn['username']));
- $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($obj_props['superadmin'] === 1) {
- $_SESSION["mailcow_cc_role"] = "admin";
- }
- elseif ($obj_props['superadmin'] === 0) {
- $_SESSION["mailcow_cc_role"] = "domainadmin";
- }
- else {
- $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
- $stmt->execute(array(':username' => $process_webauthn['username']));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (!empty($row['username'])) {
- $_SESSION["mailcow_cc_role"] = "user";
- } else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('webauthn_verification_failed', 'could not determine user role')
- );
- return false;
- }
- }
-
- if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
- );
- return false;
- }
-
- $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
- $_SESSION['tfa_id'] = $process_webauthn['id'];
- $_SESSION['authReq'] = null;
- unset($_SESSION["challenge"]);
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array("webauthn_login"),
- 'msg' => array('logged_in_as', $process_webauthn['username'])
- );
- return true;
- break;
- default:
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $username, '*'),
- 'msg' => 'unknown_tfa_method'
- );
- return false;
- break;
- }
-
- return false;
- } else {
- // delete old keys that used u2f
- $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
- $stmt->execute(array(':username' => $username));
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- if (count($rows) == 0) return false;
-
- $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
- $stmt->execute(array(':username' => $username));
- return true;
- }
-}
-function admin_api($access, $action, $data = null) {
- global $pdo;
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'access_denied'
- );
- return false;
- }
- if ($access !== "ro" && $access !== "rw") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'invalid access type'
- );
- return false;
- }
- if ($action == "edit") {
- $active = (!empty($data['active'])) ? 1 : 0;
- $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
- $allow_from = array();
- if (isset($data['allow_from'])) {
- $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
- }
- foreach ($allow_from as $key => $val) {
- if (empty($val)) {
- unset($allow_from[$key]);
- continue;
- }
- if (valid_network($val) !== true) {
- $_SESSION['return'][] = array(
- 'type' => 'warning',
- 'log' => array(__FUNCTION__, $data),
- 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
- );
- unset($allow_from[$key]);
- continue;
- }
- }
- $allow_from = implode(',', array_unique(array_filter($allow_from)));
- if (empty($allow_from) && $skip_ip_check == 0) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $data),
- 'msg' => 'ip_list_empty'
- );
- return false;
- }
- $api_key = implode('-', array(
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3)))
- ));
- $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
- $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
- if (empty($num_results)) {
- $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
- VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
- $stmt->execute(array(
- ':api_key' => $api_key,
- ':skip_ip_check' => $skip_ip_check,
- ':active' => $active,
- ':allow_from' => $allow_from,
- ':access' => $access
- ));
- }
- else {
- if ($skip_ip_check == 0) {
- $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
- `active` = :active,
- `allow_from` = :allow_from
- WHERE `access` = :access;");
- $stmt->execute(array(
- ':active' => $active,
- ':skip_ip_check' => $skip_ip_check,
- ':allow_from' => $allow_from,
- ':access' => $access
- ));
- }
- else {
- $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
- `active` = :active
- WHERE `access` = :access;");
- $stmt->execute(array(
- ':active' => $active,
- ':skip_ip_check' => $skip_ip_check,
- ':access' => $access
- ));
- }
- }
- }
- elseif ($action == "regen_key") {
- $api_key = implode('-', array(
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3))),
- strtoupper(bin2hex(random_bytes(3)))
- ));
- $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
- $stmt->execute(array(
- ':api_key' => $api_key,
- ':access' => $access
- ));
- }
- elseif ($action == "get") {
- $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
- $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($apidata !== false) {
- $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
- }
- return $apidata;
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $data),
- 'msg' => 'admin_api_modified'
- );
-}
-function license($action, $data = null) {
- global $pdo;
- global $redis;
- global $lang;
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'access_denied'
- );
- return false;
- }
- switch ($action) {
- case "verify":
- // Keep result until revalidate button is pressed or session expired
- $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
- $versions = $stmt->fetch(PDO::FETCH_ASSOC);
- $post = array('guid' => $versions['version']);
- $curl = curl_init('https://verify.mailcow.email');
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
- curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
- $response = curl_exec($curl);
- curl_close($curl);
- $json_return = json_decode($response, true);
- if ($response && $json_return) {
- if ($json_return['response'] === "ok") {
- $_SESSION['gal']['valid'] = "true";
- $_SESSION['gal']['c'] = $json_return['c'];
- $_SESSION['gal']['s'] = $json_return['s'];
- if ($json_return['m'] == 'NoMoore') {
- $_SESSION['gal']['m'] = '🐄';
- }
- else {
- $_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
- }
- }
- elseif ($json_return['response'] === "invalid") {
- $_SESSION['gal']['valid'] = "false";
- $_SESSION['gal']['c'] = $lang['mailbox']['no'];
- $_SESSION['gal']['s'] = $lang['mailbox']['no'];
- $_SESSION['gal']['m'] = $lang['mailbox']['no'];
- }
- }
- else {
- $_SESSION['gal']['valid'] = "false";
- $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
- $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
- $_SESSION['gal']['m'] = $lang['danger']['temp_error'];
- }
- try {
- // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
- $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- return $_SESSION['gal']['valid'];
- break;
- case "guid":
- $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
- $versions = $stmt->fetch(PDO::FETCH_ASSOC);
- return $versions['version'];
- break;
- }
-}
-function rspamd_ui($action, $data = null) {
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__),
- 'msg' => 'access_denied'
- );
- return false;
- }
- switch ($action) {
- case "edit":
- $rspamd_ui_pass = $data['rspamd_ui_pass'];
- $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
- if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, '*', '*'),
- 'msg' => 'password_empty'
- );
- return false;
- }
- if ($rspamd_ui_pass != $rspamd_ui_pass2) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, '*', '*'),
- 'msg' => 'password_mismatch'
- );
- return false;
- }
- if (strlen($rspamd_ui_pass) < 6) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, '*', '*'),
- 'msg' => 'rspamd_ui_pw_length'
- );
- return false;
- }
- $docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
- if ($docker_return_array = json_decode($docker_return, true)) {
- if ($docker_return_array['type'] == 'success') {
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, '*', '*'),
- 'msg' => 'rspamd_ui_pw_set'
- );
- return true;
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => $docker_return_array['type'],
- 'log' => array(__FUNCTION__, '*', '*'),
- 'msg' => $docker_return_array['msg']
- );
- return false;
- }
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, '*', '*'),
- 'msg' => 'unknown'
- );
- return false;
- }
- break;
- }
-}
-
-function get_logs($application, $lines = false) {
- if ($lines === false) {
- $lines = $GLOBALS['LOG_LINES'] - 1;
- }
- elseif(is_numeric($lines) && $lines >= 1) {
- $lines = abs(intval($lines) - 1);
- }
- else {
- list ($from, $to) = explode('-', $lines);
- $from = intval($from);
- $to = intval($to);
- if ($from < 1 || $to < $from) { return false; }
- }
- global $redis;
- global $pdo;
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- return false;
- }
- // SQL
- if ($application == "mailcow-ui") {
- if (isset($from) && isset($to)) {
- $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
- $stmt->execute(array(
- ':from' => $from - 1,
- ':to' => $to
- ));
- $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
- else {
- $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
- $stmt->execute(array(
- ':lines' => $lines + 1,
- ));
- $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
- if (is_array($data)) {
- return $data;
- }
- }
- if ($application == "sasl") {
- if (isset($from) && isset($to)) {
- $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :from, :to");
- $stmt->execute(array(
- ':from' => $from - 1,
- ':to' => $to
- ));
- $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
- else {
- $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :lines");
- $stmt->execute(array(
- ':lines' => $lines + 1,
- ));
- $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
- }
- if (is_array($data)) {
- return $data;
- }
- }
- // Redis
- if ($application == "dovecot-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "postfix-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "sogo-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('SOGO_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "watchdog-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "acme-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('ACME_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "ratelimited") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('RL_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "api-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('API_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "netfilter-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "autodiscover-mailcow") {
- if (isset($from) && isset($to)) {
- $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
- }
- else {
- $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
- }
- if ($data) {
- foreach ($data as $json_line) {
- $data_array[] = json_decode($json_line, true);
- }
- return $data_array;
- }
- }
- if ($application == "rspamd-history") {
- $curl = curl_init();
- curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
- if (!is_numeric($lines)) {
- list ($from, $to) = explode('-', $lines);
- curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
- }
- else {
- curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
- }
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
- $history = curl_exec($curl);
- if (!curl_errno($curl)) {
- $data_array = json_decode($history, true);
- curl_close($curl);
- return $data_array['rows'];
- }
- curl_close($curl);
- return false;
- }
- if ($application == "rspamd-stats") {
- $curl = curl_init();
- curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
- curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
- $stats = curl_exec($curl);
- if (!curl_errno($curl)) {
- $data_array = json_decode($stats, true);
- curl_close($curl);
- return $data_array;
- }
- curl_close($curl);
- return false;
- }
- return false;
-}
-function getGUID() {
- if (function_exists('com_create_guid')) {
- return com_create_guid();
- }
- mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
- $charid = strtoupper(md5(uniqid(rand(), true)));
- $hyphen = chr(45);// "-"
- return substr($charid, 0, 8).$hyphen
- .substr($charid, 8, 4).$hyphen
- .substr($charid,12, 4).$hyphen
- .substr($charid,16, 4).$hyphen
- .substr($charid,20,12);
-}
-function solr_status() {
- $curl = curl_init();
- $endpoint = 'http://solr:8983/solr/admin/cores';
- $params = array(
- 'action' => 'STATUS',
- 'core' => 'dovecot-fts',
- 'indexInfo' => 'true'
- );
- $url = $endpoint . '?' . http_build_query($params);
- curl_setopt($curl, CURLOPT_URL, $url);
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($curl, CURLOPT_POST, 0);
- curl_setopt($curl, CURLOPT_TIMEOUT, 10);
- $response_core = curl_exec($curl);
- if ($response_core === false) {
- $err = curl_error($curl);
- curl_close($curl);
- return false;
- }
- else {
- curl_close($curl);
- $curl = curl_init();
- $status_core = json_decode($response_core, true);
- $url = 'http://solr:8983/solr/admin/info/system';
- curl_setopt($curl, CURLOPT_URL, $url);
- curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($curl, CURLOPT_POST, 0);
- curl_setopt($curl, CURLOPT_TIMEOUT, 10);
- $response_sysinfo = curl_exec($curl);
- if ($response_sysinfo === false) {
- $err = curl_error($curl);
- curl_close($curl);
- return false;
- }
- else {
- curl_close($curl);
- $status_sysinfo = json_decode($response_sysinfo, true);
- $status = array_merge($status_core, $status_sysinfo);
- return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
- }
- return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
- }
- return false;
-}
-
-function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
- $now = time();
- foreach (glob($folder) as $filename) {
- if(strpos($filename, $ignore) !== false) {
- continue;
- }
- if (is_file($filename)) {
- if ($now - filemtime($filename) >= 60 * 60) {
- unlink($filename);
- }
- }
- }
-}
-
-function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
- $now = time();
- foreach (glob($folder) as $filename) {
- if(strpos($filename, $ignore) !== false) {
- continue;
- }
- if (is_file($filename)) {
- if ($now - filemtime($filename) >= 60 * 60) {
- unlink($filename);
- }
- }
- }
-}
-
-?>
+1) {
+ $bits=(int)($ar[1]);
+ }
+ else {
+ $bits = $iplen * 8;
+ }
+ for ($c=0; $bits>0; $c++) {
+ $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
+ if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0) {
+ continue 2;
+ }
+ $bits-=8;
+ }
+ return true;
+ }
+ return false;
+}
+function hash_password($password) {
+ // default_pass_scheme is determined in vars.inc.php (or corresponding local file)
+ // in case default pass scheme is not defined, falling back to BLF-CRYPT.
+ global $default_pass_scheme;
+ $pw_hash = NULL;
+ // support pre-hashed passwords
+ if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
+ $pw_hash = $password;
+ }
+ else {
+ switch (strtoupper($default_pass_scheme)) {
+ case "SSHA":
+ $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+ $pw_hash = "{SSHA}".base64_encode(hash('sha1', $password . $salt_str, true) . $salt_str);
+ break;
+ case "SSHA256":
+ $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+ $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
+ break;
+ case "SSHA512":
+ $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+ $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
+ break;
+ case "BLF-CRYPT":
+ default:
+ $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
+ break;
+ }
+ }
+ return $pw_hash;
+}
+function password_complexity($_action, $_data = null) {
+ global $redis;
+ global $lang;
+ switch ($_action) {
+ case 'edit':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $is_now = password_complexity('get');
+ if (!empty($is_now)) {
+ $length = (isset($_data['length']) && intval($_data['length']) >= 3) ? intval($_data['length']) : $is_now['length'];
+ $chars = (isset($_data['chars'])) ? intval($_data['chars']) : $is_now['chars'];
+ $lowerupper = (isset($_data['lowerupper'])) ? intval($_data['lowerupper']) : $is_now['lowerupper'];
+ $special_chars = (isset($_data['special_chars'])) ? intval($_data['special_chars']) : $is_now['special_chars'];
+ $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
+ }
+ try {
+ $redis->hMSet('PASSWD_POLICY', [
+ 'length' => $length,
+ 'chars' => $chars,
+ 'special_chars' => $special_chars,
+ 'lowerupper' => $lowerupper,
+ 'numbers' => $numbers
+ ]);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => 'password_policy_saved'
+ );
+ break;
+ case 'get':
+ try {
+ $length = $redis->hGet('PASSWD_POLICY', 'length');
+ $chars = $redis->hGet('PASSWD_POLICY', 'chars');
+ $special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
+ $lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
+ $numbers = $redis->hGet('PASSWD_POLICY', 'numbers');
+ return array(
+ 'length' => $length,
+ 'chars' => $chars,
+ 'special_chars' => $special_chars,
+ 'lowerupper' => $lowerupper,
+ 'numbers' => $numbers
+ );
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ return false;
+ break;
+ case 'html':
+ $policies = password_complexity('get');
+ foreach ($policies as $name => $value) {
+ if ($value != 0) {
+ $policy_text[] = sprintf($lang['admin']["password_policy_$name"], $value);
+ }
+ }
+ return '- ' . implode(' - ', (array)$policy_text) . '
';
+ break;
+ }
+}
+function password_check($password1, $password2) {
+ $password_complexity = password_complexity('get');
+
+ if (empty($password1) || empty($password2)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type),
+ 'msg' => 'password_complexity'
+ );
+ return false;
+ }
+
+ if ($password1 != $password2) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type),
+ 'msg' => 'password_mismatch'
+ );
+ return false;
+ }
+
+ $given_password['length'] = strlen($password1);
+ $given_password['special_chars'] = preg_match('/[^a-zA-Z\d]/', $password1);
+ $given_password['chars'] = preg_match('/[a-zA-Z]/',$password1);
+ $given_password['numbers'] = preg_match('/\d/', $password1);
+ $lower = strlen(preg_replace("/[^a-z]/", '', $password1));
+ $upper = strlen(preg_replace("/[^A-Z]/", '', $password1));
+ $given_password['lowerupper'] = ($lower > 0 && $upper > 0) ? true : false;
+
+ if (
+ ($given_password['length'] < $password_complexity['length']) ||
+ ($password_complexity['special_chars'] == 1 && (intval($given_password['special_chars']) != $password_complexity['special_chars'])) ||
+ ($password_complexity['chars'] == 1 && (intval($given_password['chars']) != $password_complexity['chars'])) ||
+ ($password_complexity['numbers'] == 1 && (intval($given_password['numbers']) != $password_complexity['numbers'])) ||
+ ($password_complexity['lowerupper'] == 1 && (intval($given_password['lowerupper']) != $password_complexity['lowerupper']))
+ ) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type),
+ 'msg' => 'password_complexity'
+ );
+ return false;
+ }
+
+ return true;
+}
+function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
+ global $pdo;
+ global $redis;
+ $sasl_limit_days = intval($sasl_limit_days);
+ switch ($action) {
+ case 'get':
+ if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+ $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service`, `app_password`, MAX(`app_passwd`.`name`) as `app_password_name` FROM `sasl_log`
+ LEFT OUTER JOIN `app_passwd` on `sasl_log`.`app_password` = `app_passwd`.`id`
+ WHERE `username` = :username
+ AND HOUR(TIMEDIFF(NOW(), `datetime`)) < :sasl_limit_days
+ GROUP BY `real_rip`, `service`, `app_password`
+ ORDER BY `datetime` DESC;');
+ $stmt->execute(array(':username' => $username, ':sasl_limit_days' => ($sasl_limit_days * 24)));
+ $sasl = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($sasl as $k => $v) {
+ if (!filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+ $sasl[$k]['real_rip'] = 'Web/EAS/Internal (' . $sasl[$k]['real_rip'] . ')';
+ }
+ elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+ try {
+ $sasl[$k]['location'] = $redis->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ if (!$sasl[$k]['location']) {
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
+ curl_setopt($curl, CURLOPT_TIMEOUT, 5);
+ $ip_data = curl_exec($curl);
+ if (!curl_errno($curl)) {
+ $ip_data_array = json_decode($ip_data, true);
+ if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) {
+ $sasl[$k]['location'] = $ip_data_array['location']['shortcountry'];
+ try {
+ $redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ curl_close($curl);
+ return false;
+ }
+ }
+ }
+ curl_close($curl);
+ }
+ }
+ }
+ }
+ else {
+ $sasl = array();
+ }
+ if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
+ $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
+ WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
+ AND JSON_EXTRACT(`call`, "$[1]") = :username
+ AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET :offset');
+ $stmt->execute(array(
+ ':username' => $username,
+ ':offset' => $ui_offset
+ ));
+ $ui = $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+ else {
+ $ui = array();
+ }
+
+ return array('ui' => $ui, 'sasl' => $sasl);
+ break;
+ case 'reset':
+ if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+ $stmt = $pdo->prepare('DELETE FROM `sasl_log`
+ WHERE `username` = :username');
+ $stmt->execute(array(':username' => $username));
+ }
+ if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
+ $stmt = $pdo->prepare('DELETE FROM `logs`
+ WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
+ AND JSON_EXTRACT(`call`, "$[1]") = :username
+ AND `type` = "success"');
+ $stmt->execute(array(':username' => $username));
+ }
+ return true;
+ break;
+ }
+
+}
+function flush_memcached() {
+ try {
+ $m = new Memcached();
+ $m->addServer('memcached', 11211);
+ $m->flush();
+ }
+ catch ( Exception $e ) {
+ // Dunno
+ }
+}
+function sys_mail($_data) {
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $excludes = $_data['mass_exclude'];
+ $includes = $_data['mass_include'];
+ $mailboxes = array();
+ $mass_from = $_data['mass_from'];
+ $mass_text = $_data['mass_text'];
+ $mass_html = $_data['mass_html'];
+ $mass_subject = $_data['mass_subject'];
+ if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'from_invalid'
+ );
+ return false;
+ }
+ if (empty($mass_subject)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'subject_empty'
+ );
+ return false;
+ }
+ if (empty($mass_text)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'text_empty'
+ );
+ return false;
+ }
+ $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
+ foreach ($domains as $domain) {
+ foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
+ $mailboxes[] = $mailbox;
+ }
+ }
+ if (!empty($includes)) {
+ $rcpts = array_intersect($mailboxes, $includes);
+ }
+ elseif (!empty($excludes)) {
+ $rcpts = array_diff($mailboxes, $excludes);
+ }
+ else {
+ $rcpts = $mailboxes;
+ }
+ if (!empty($rcpts)) {
+ ini_set('max_execution_time', 0);
+ ini_set('max_input_time', 0);
+ $mail = new PHPMailer;
+ $mail->Timeout = 10;
+ $mail->SMTPOptions = array(
+ 'ssl' => array(
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'allow_self_signed' => true
+ )
+ );
+ $mail->isSMTP();
+ $mail->Host = 'dovecot-mailcow';
+ $mail->SMTPAuth = false;
+ $mail->Port = 24;
+ $mail->setFrom($mass_from);
+ $mail->Subject = $mass_subject;
+ $mail->CharSet ="UTF-8";
+ if (!empty($mass_html)) {
+ $mail->Body = $mass_html;
+ $mail->AltBody = $mass_text;
+ }
+ else {
+ $mail->Body = $mass_text;
+ }
+ $mail->XMailer = 'MooMassMail';
+ foreach ($rcpts as $rcpt) {
+ $mail->AddAddress($rcpt);
+ if (!$mail->send()) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
+ );
+ }
+ $mail->ClearAllRecipients();
+ }
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
+ );
+}
+function logger($_data = false) {
+ /*
+ logger() will be called as last function
+ To manually log a message, logger needs to be called like below.
+
+ logger(array(
+ 'return' => array(
+ array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => $err
+ )
+ )
+ ));
+
+ These messages will not be printed as alert box.
+ To do so, push them to $_SESSION['return'] and do not call logger as they will be included automatically:
+
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => $err
+ );
+ */
+ global $pdo;
+ if (!$_data) {
+ $_data = $_SESSION;
+ }
+ if (!empty($_data['return'])) {
+ $task = substr(strtoupper(md5(uniqid(rand(), true))), 0, 6);
+ foreach ($_data['return'] as $return) {
+ $type = $return['type'];
+ $msg = null;
+ if (isset($return['msg'])) {
+ $msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
+ }
+ $call = null;
+ if (isset($return['log'])) {
+ $call = json_encode($return['log'], JSON_UNESCAPED_UNICODE);
+ }
+ if (!empty($_SESSION["dual-login"]["username"])) {
+ $user = $_SESSION["dual-login"]["username"] . ' => ' . $_SESSION['mailcow_cc_username'];
+ $role = $_SESSION["dual-login"]["role"] . ' => ' . $_SESSION['mailcow_cc_role'];
+ }
+ elseif (!empty($_SESSION['mailcow_cc_username'])) {
+ $user = $_SESSION['mailcow_cc_username'];
+ $role = $_SESSION['mailcow_cc_role'];
+ }
+ else {
+ $user = 'unauthenticated';
+ $role = 'unauthenticated';
+ }
+ // We cannot log when logs is missing...
+ try {
+ $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
+ (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
+ $stmt->execute(array(
+ ':type' => $type,
+ ':task' => $task,
+ ':call' => $call,
+ ':msg' => $msg,
+ ':user' => $user,
+ ':role' => $role,
+ ':remote' => get_remote_ip()
+ ));
+ }
+ catch (Exception $e) {
+ // Do nothing
+ }
+ }
+ }
+ else {
+ return true;
+ }
+}
+function hasDomainAccess($username, $role, $domain) {
+ global $pdo;
+ if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+ return false;
+ }
+ if (empty($domain) || !is_valid_domain_name($domain)) {
+ return false;
+ }
+ if ($role != 'admin' && $role != 'domainadmin') {
+ return false;
+ }
+ if ($role == 'admin') {
+ $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+ WHERE `domain` = :domain");
+ $stmt->execute(array(':domain' => $domain));
+ $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+ $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
+ WHERE `alias_domain` = :domain");
+ $stmt->execute(array(':domain' => $domain));
+ $num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
+ if ($num_results != 0) {
+ return true;
+ }
+ }
+ elseif ($role == 'domainadmin') {
+ $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
+ WHERE (
+ `active`='1'
+ AND `username` = :username
+ AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
+ )");
+ $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
+ $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+ if (!empty($num_results)) {
+ return true;
+ }
+ }
+ return false;
+}
+function hasMailboxObjectAccess($username, $role, $object) {
+ global $pdo;
+ if (empty($username) || empty($role) || empty($object)) {
+ return false;
+ }
+ if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+ return false;
+ }
+ if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
+ return false;
+ }
+ if ($username == $object) {
+ return true;
+ }
+ $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
+ $stmt->execute(array(':object' => $object));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
+ return true;
+ }
+ return false;
+}
+// does also verify mailboxes as a mailbox is a alias == goto
+function hasAliasObjectAccess($username, $role, $object) {
+ global $pdo;
+ if (empty($username) || empty($role) || empty($object)) {
+ return false;
+ }
+ if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+ return false;
+ }
+ if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
+ return false;
+ }
+ $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
+ $stmt->execute(array(':object' => $object));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
+ return true;
+ }
+ return false;
+}
+function pem_to_der($pem_key) {
+ // Need to remove BEGIN/END PUBLIC KEY
+ $lines = explode("\n", trim($pem_key));
+ unset($lines[count($lines)-1]);
+ unset($lines[0]);
+ return base64_decode(implode('', $lines));
+}
+function expand_ipv6($ip) {
+ $hex = unpack("H*hex", inet_pton($ip));
+ $ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
+ return $ip;
+}
+function generate_tlsa_digest($hostname, $port, $starttls = null) {
+ if (!is_valid_domain_name($hostname)) {
+ return "Not a valid hostname";
+ }
+ if (empty($starttls)) {
+ $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true)));
+ $stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
+ if (!$stream) {
+ $error_msg = isset($error_msg) ? $error_msg : '-';
+ return $error_nr . ': ' . $error_msg;
+ }
+ }
+ else {
+ $stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
+ if (!$stream) {
+ return $error_nr . ': ' . $error_msg;
+ }
+ $banner = fread($stream, 512 );
+ if (preg_match("/^220/i", $banner)) { // SMTP
+ fwrite($stream,"HELO tlsa.generator.local\r\n");
+ fread($stream, 512);
+ fwrite($stream,"STARTTLS\r\n");
+ fread($stream, 512);
+ }
+ elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP
+ fwrite($stream,"A1 STARTTLS\r\n");
+ fread($stream, 512);
+ }
+ elseif (preg_match("/^\+OK/", $banner)) { // POP3
+ fwrite($stream,"STLS\r\n");
+ fread($stream, 512);
+ }
+ elseif (preg_match("/^OK/m", $banner)) { // Sieve
+ fwrite($stream,"STARTTLS\r\n");
+ fread($stream, 512);
+ }
+ else {
+ return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"';
+ }
+ // Upgrade connection
+ stream_set_blocking($stream, true);
+ stream_context_set_option($stream, 'ssl', 'capture_peer_cert', true);
+ stream_context_set_option($stream, 'ssl', 'verify_peer', false);
+ stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
+ stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
+ stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
+ stream_set_blocking($stream, false);
+ }
+ $params = stream_context_get_params($stream);
+ if (!empty($params['options']['ssl']['peer_certificate'])) {
+ $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']);
+ // We cannot get ['rsa']['n'], the binary data would contain BEGIN/END PUBLIC KEY
+ $key_data = openssl_pkey_get_details($key_resource)['key'];
+ return '3 1 1 ' . openssl_digest(pem_to_der($key_data), 'sha256');
+ }
+ else {
+ return 'Error: Cannot read peer certificate';
+ }
+}
+function alertbox_log_parser($_data) {
+ global $lang;
+ if (isset($_data['return'])) {
+ foreach ($_data['return'] as $return) {
+ // Get type
+ $type = $return['type'];
+ // If a lang[type][msg] string exists, use it as message
+ if (isset($return['type']) && isset($return['msg']) && !is_array($return['msg'])) {
+ if (isset($lang[$return['type']][$return['msg']])) {
+ $msg = $lang[$return['type']][$return['msg']];
+ }
+ else {
+ $msg = $return['msg'];
+ }
+ }
+ // If msg is an array, use first element as language string and run printf on it with remaining array elements
+ elseif (is_array($return['msg'])) {
+ $msg = array_shift($return['msg']);
+ $msg = vsprintf(
+ $lang[$return['type']][$msg],
+ $return['msg']
+ );
+ }
+ else {
+ $msg = '-';
+ }
+ $log_array[] = array('msg' => $msg, 'type' => json_encode($type));
+ }
+ if (!empty($log_array)) {
+ return $log_array;
+ }
+ }
+ return false;
+}
+function verify_salted_hash($hash, $password, $algo, $salt_length) {
+ // Decode hash
+ $dhash = base64_decode($hash);
+ // Get first n bytes of binary which equals a SSHA hash
+ $ohash = substr($dhash, 0, $salt_length);
+ // Remove SSHA hash from decoded hash to get original salt string
+ $osalt = str_replace($ohash, '', $dhash);
+ // Check single salted SSHA hash against extracted hash
+ if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
+ return true;
+ }
+ return false;
+}
+function verify_hash($hash, $password) {
+ if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
+ $scheme = strtoupper($hash_array[1]);
+ $hash = $hash_array[2];
+ switch ($scheme) {
+ case "ARGON2I":
+ case "ARGON2ID":
+ case "BLF-CRYPT":
+ case "CRYPT":
+ case "DES-CRYPT":
+ case "MD5-CRYPT":
+ case "MD5":
+ case "SHA256-CRYPT":
+ case "SHA512-CRYPT":
+ return password_verify($password, $hash);
+
+ case "CLEAR":
+ case "CLEARTEXT":
+ case "PLAIN":
+ return $password == $hash;
+
+ case "LDAP-MD5":
+ $hash = base64_decode($hash);
+ return hash_equals(hash('md5', $password, true), $hash);
+
+ case "PBKDF2":
+ $components = explode('$', $hash);
+ $salt = $components[2];
+ $rounds = $components[3];
+ $hash = $components[4];
+ return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
+
+ case "PLAIN-MD4":
+ return hash_equals(hash('md4', $password), $hash);
+
+ case "PLAIN-MD5":
+ return md5($password) == $hash;
+
+ case "PLAIN-TRUNC":
+ $components = explode('-', $hash);
+ if (count($components) > 1) {
+ $trunc_len = $components[0];
+ $trunc_password = $components[1];
+
+ return substr($password, 0, $trunc_len) == $trunc_password;
+ } else {
+ return $password == $hash;
+ }
+
+ case "SHA":
+ case "SHA1":
+ case "SHA256":
+ case "SHA512":
+ // SHA is an alias for SHA1
+ $scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
+ $hash = base64_decode($hash);
+ return hash_equals(hash($scheme, $password, true), $hash);
+
+ case "SMD5":
+ return verify_salted_hash($hash, $password, 'md5', 16);
+
+ case "SSHA":
+ return verify_salted_hash($hash, $password, 'sha1', 20);
+
+ case "SSHA256":
+ return verify_salted_hash($hash, $password, 'sha256', 32);
+
+ case "SSHA512":
+ return verify_salted_hash($hash, $password, 'sha512', 64);
+
+ default:
+ return false;
+ }
+ }
+ return false;
+}
+function check_login($user, $pass, $app_passwd_data = false) {
+ global $pdo;
+ global $redis;
+ global $imap_server;
+
+ if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'malformed_username'
+ );
+ return false;
+ }
+
+ // Validate admin
+ $user = strtolower(trim($user));
+ $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+ WHERE `superadmin` = '1'
+ AND `active` = '1'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($rows as $row) {
+ // verify password
+ if (verify_hash($row['password'], $pass)) {
+ // check for tfa authenticators
+ $authenticators = get_tfa($user);
+ if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
+ // active tfa authenticators found, set pending user login
+ $_SESSION['pending_mailcow_cc_username'] = $user;
+ $_SESSION['pending_mailcow_cc_role'] = "admin";
+ $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+ unset($_SESSION['ldelay']);
+ $_SESSION['return'][] = array(
+ 'type' => 'info',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'awaiting_tfa_confirmation'
+ );
+ return "pending";
+ } else {
+ unset($_SESSION['ldelay']);
+ // Reactivate TFA if it was set to "deactivate TFA for next login"
+ $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => array('logged_in_as', $user)
+ );
+ return "admin";
+ }
+ }
+ }
+
+ // Validate domain admin
+ $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+ WHERE `superadmin` = '0'
+ AND `active`='1'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($rows as $row) {
+ // verify password
+ if (verify_hash($row['password'], $pass) !== false) {
+ // check for tfa authenticators
+ $authenticators = get_tfa($user);
+ if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
+ $_SESSION['pending_mailcow_cc_username'] = $user;
+ $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
+ $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+ unset($_SESSION['ldelay']);
+ $_SESSION['return'][] = array(
+ 'type' => 'info',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'awaiting_tfa_confirmation'
+ );
+ return "pending";
+ }
+ else {
+ unset($_SESSION['ldelay']);
+ // Reactivate TFA if it was set to "deactivate TFA for next login"
+ $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => array('logged_in_as', $user)
+ );
+ return "domainadmin";
+ }
+ }
+ }
+
+ // Validate mailbox user
+ $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+ INNER JOIN domain on mailbox.domain = domain.domain
+ WHERE `kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active`='1'
+ AND `domain`.`active`='1'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if ($app_passwd_data['eas'] === true) {
+ $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
+ INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
+ INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
+ WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active` = '1'
+ AND `domain`.`active` = '1'
+ AND `app_passwd`.`active` = '1'
+ AND `app_passwd`.`eas_access` = '1'
+ AND `app_passwd`.`mailbox` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
+ }
+ elseif ($app_passwd_data['dav'] === true) {
+ $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
+ INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
+ INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
+ WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active` = '1'
+ AND `domain`.`active` = '1'
+ AND `app_passwd`.`active` = '1'
+ AND `app_passwd`.`dav_access` = '1'
+ AND `app_passwd`.`mailbox` = :user");
+ $stmt->execute(array(':user' => $user));
+ $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
+ }
+ foreach ($rows as $row) {
+ // verify password
+ if (verify_hash($row['password'], $pass) !== false) {
+ if (!array_key_exists("app_passwd_id", $row)){
+ // password is not a app password
+ // check for tfa authenticators
+ $authenticators = get_tfa($user);
+ if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&
+ $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {
+ // authenticators found, init TFA flow
+ $_SESSION['pending_mailcow_cc_username'] = $user;
+ $_SESSION['pending_mailcow_cc_role'] = "user";
+ $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+ unset($_SESSION['ldelay']);
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => array('logged_in_as', $user)
+ );
+ return "pending";
+ } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
+ // no authenticators found, login successfull
+ // Reactivate TFA if it was set to "deactivate TFA for next login"
+ $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+ $stmt->execute(array(':user' => $user));
+
+ unset($_SESSION['ldelay']);
+ return "user";
+ }
+ } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
+ // password is a app password
+ $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
+ $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
+ $stmt->execute(array(
+ ':service' => $service,
+ ':app_id' => $row['app_passwd_id'],
+ ':username' => $user,
+ ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
+ ));
+
+ unset($_SESSION['ldelay']);
+ return "user";
+ }
+ }
+ }
+
+ if (!isset($_SESSION['ldelay'])) {
+ $_SESSION['ldelay'] = "0";
+ $redis->publish("NETFILTER_CHANNEL", "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;
+ $redis->publish("NETFILTER_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ }
+
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'login_failed'
+ );
+
+ sleep($_SESSION['ldelay']);
+ return false;
+}
+function formatBytes($size, $precision = 2) {
+ if(!is_numeric($size)) {
+ return "0";
+ }
+ $base = log($size, 1024);
+ $suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
+ if ($size == "0") {
+ return "0";
+ }
+ return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
+}
+function update_sogo_static_view() {
+ if (getenv('SKIP_SOGO') == "y") {
+ return true;
+ }
+ global $pdo;
+ global $lang;
+ $stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
+ WHERE TABLE_NAME = 'sogo_view'");
+ $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+ if ($num_results != 0) {
+ $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
+ SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
+ $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
+ }
+ flush_memcached();
+}
+function edit_user_account($_data) {
+ global $lang;
+ global $pdo;
+ $_data_log = $_data;
+ !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
+ !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
+ !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
+ $username = $_SESSION['mailcow_cc_username'];
+ $role = $_SESSION['mailcow_cc_role'];
+ $password_old = $_data['user_old_pass'];
+ if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+ WHERE `kind` NOT REGEXP 'location|thing|group'
+ AND `username` = :user");
+ $stmt->execute(array(':user' => $username));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (!verify_hash($row['password'], $password_old)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
+ $password_new = $_data['user_new_pass'];
+ $password_new2 = $_data['user_new_pass2'];
+ if (password_check($password_new, $password_new2) !== true) {
+ return false;
+ }
+ $password_hashed = hash_password($password_new);
+ $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed,
+ `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0'),
+ `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
+ WHERE `username` = :username");
+ $stmt->execute(array(
+ ':password_hashed' => $password_hashed,
+ ':username' => $username
+ ));
+ }
+ update_sogo_static_view();
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('mailbox_modified', htmlspecialchars($username))
+ );
+}
+function user_get_alias_details($username) {
+ global $pdo;
+ global $lang;
+ $data['direct_aliases'] = array();
+ $data['shared_aliases'] = array();
+ if ($_SESSION['mailcow_cc_role'] == "user") {
+ $username = $_SESSION['mailcow_cc_username'];
+ }
+ if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+ return false;
+ }
+ if (!hasMailboxObjectAccess($username, $_SESSION['mailcow_cc_role'], $username)) {
+ return false;
+ }
+ $data['address'] = $username;
+ $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
+ WHERE `goto` REGEXP :username_goto
+ AND `address` NOT LIKE '@%'
+ AND `goto` != :username_goto2
+ AND `address` != :username_address");
+ $stmt->execute(array(
+ ':username_goto' => '(^|,)'.$username.'($|,)',
+ ':username_goto2' => $username,
+ ':username_address' => $username
+ ));
+ $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($run)) {
+ $data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
+ //$data['shared_aliases'][] = $row['shared_aliases'];
+ }
+
+ $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
+ WHERE `goto` = :username_goto
+ AND `address` NOT LIKE '@%'
+ AND `address` != :username_address");
+ $stmt->execute(
+ array(
+ ':username_goto' => $username,
+ ':username_address' => $username
+ ));
+ $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($run)) {
+ $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
+ }
+ $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
+ LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
+ WHERE `username` = :username ;");
+ $stmt->execute(array(':username' => $username));
+ $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($run)) {
+ if (empty($row['ad_alias'])) {
+ continue;
+ }
+ $data['direct_aliases'][$row['ad_alias']]['public_comment'] = $lang['add']['alias_domain'];
+ $data['alias_domains'][] = $row['alias_domain'];
+ }
+ $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
+ $stmt->execute(array(':username' => $username));
+ $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($run)) {
+ $data['aliases_also_send_as'] = $row['send_as'];
+ }
+ $stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ''), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
+ $stmt->execute(array(':username' => $username));
+ $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($run)) {
+ $data['aliases_send_as_all'] = $row['send_as'];
+ }
+ $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '') as `address` FROM `alias` WHERE `goto` REGEXP :username AND `address` LIKE '@%';");
+ $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
+ $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while ($row = array_shift($run)) {
+ $data['is_catch_all'] = $row['address'];
+ }
+ return $data;
+}
+function is_valid_domain_name($domain_name) {
+ if (empty($domain_name)) {
+ return false;
+ }
+ $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
+ return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
+ && preg_match("/^.{1,253}$/", $domain_name)
+ && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
+}
+function set_tfa($_data) {
+ global $pdo;
+ global $yubi;
+ global $tfa;
+ $_data_log = $_data;
+ $access_denied = null;
+ !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
+ $username = $_SESSION['mailcow_cc_username'];
+
+ // check for empty user and role
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
+
+ // check admin confirm password
+ if ($access_denied === null) {
+ $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+ WHERE `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($row) {
+ if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
+ else $access_denied = false;
+ }
+ }
+
+ // check mailbox confirm password
+ if ($access_denied === null) {
+ $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+ WHERE `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($row) {
+ if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
+ else $access_denied = false;
+ }
+ }
+
+ // set access_denied error
+ if ($access_denied){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ switch ($_data["tfa_method"]) {
+ case "yubi_otp":
+ $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
+ $yubico_id = $_data['yubico_id'];
+ $yubico_key = $_data['yubico_key'];
+ $yubi = new Auth_Yubico($yubico_id, $yubico_key);
+ if (!$yubi) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'tfa_token_invalid'
+ );
+ return false;
+ }
+ $yauth = $yubi->verify($_data["otp_token"]);
+ if (PEAR::isError($yauth)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('yotp_verification_failed', $yauth->getMessage())
+ );
+ return false;
+ }
+ try {
+ // We could also do a modhex translation here
+ $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
+ $stmt = $pdo->prepare("DELETE FROM `tfa`
+ WHERE `username` = :username
+ AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
+ $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+ $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
+ (:key_id, :username, 'yubi_otp', '1', :secret)");
+ $stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
+ }
+ catch (PDOException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('mysql_error', $e)
+ );
+ return false;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($username))
+ );
+ break;
+ case "totp":
+ $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
+ if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
+ //$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+ //$stmt->execute(array(':username' => $username));
+ $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
+ $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', $username)
+ );
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'totp_verification_failed'
+ );
+ }
+ break;
+ case "webauthn":
+ $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
+
+ $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
+ VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
+ $stmt->execute(array(
+ $username,
+ $key_id,
+ base64_encode($_data['registration']->credentialId),
+ $_data['registration']->credentialPublicKey,
+ $_data['registration']->certificate,
+ 0
+ ));
+
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', $username)
+ );
+ break;
+ case "none":
+ $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($username))
+ );
+ break;
+ }
+}
+function fido2($_data) {
+ global $pdo;
+ $_data_log = $_data;
+ // Not logging registration data, only actions
+ // Silent errors for "get" requests
+ switch ($_data["action"]) {
+ case "register":
+ $username = $_SESSION['mailcow_cc_username'];
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data["action"]),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute(array(
+ $username,
+ $_data['registration']->rpId,
+ $_data['registration']->credentialPublicKey,
+ $_data['registration']->certificateChain,
+ $_data['registration']->certificate,
+ $_data['registration']->certificateIssuer,
+ $_data['registration']->certificateSubject,
+ $_data['registration']->signatureCounter,
+ $_data['registration']->AAGUID,
+ $_data['registration']->credentialId)
+ );
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data["action"]),
+ 'msg' => array('object_modified', $username)
+ );
+ break;
+ case "get_user_cids":
+ // Used to exclude existing CredentialIds while registering
+ $username = $_SESSION['mailcow_cc_username'];
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
+ return false;
+ }
+ $stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $cids[] = $row['credentialId'];
+ }
+ return $cids;
+ break;
+ case "get_all_cids":
+ // Only needed when using fido2 with username
+ $stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $cids[] = $row['credentialId'];
+ }
+ return $cids;
+ break;
+ case "get_by_b64cid":
+ if (!isset($_data['cid']) || empty($_data['cid'])) {
+ return false;
+ }
+ $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE `credentialId` = :cid");
+ $stmt->execute(array(':cid' => base64_decode($_data['cid'])));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
+ return false;
+ }
+ $data['pub_key'] = $row['credentialPublicKey'];
+ $data['username'] = $row['username'];
+ $data['subject'] = $row['certificateSubject'];
+ $data['cid'] = $row['cid'];
+ return $data;
+ break;
+ case "get_friendly_names":
+ $username = $_SESSION['mailcow_cc_username'];
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
+ return false;
+ }
+ $stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $fns[] = array(
+ "subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
+ "fn" => $row['friendlyName'],
+ "cid" => $row['cid']
+ );
+ }
+ return $fns;
+ break;
+ case "unset_fido2_key":
+ $username = $_SESSION['mailcow_cc_username'];
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data["action"]),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':cid' => $_data['post_data']['unset_fido2_key']
+ ));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($username))
+ );
+ break;
+ case "edit_fn":
+ $username = $_SESSION['mailcow_cc_username'];
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data["action"]),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
+ ':cid' => $_data['fido2_attrs']['fido2_cid']
+ ));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($username))
+ );
+ break;
+ }
+}
+function unset_tfa_key($_data) {
+ // Can only unset own keys
+ // Needs at least one key left
+ global $pdo;
+ global $lang;
+ $_data_log = $_data;
+ $access_denied = null;
+ $id = intval($_data['unset_tfa_key']);
+ $username = $_SESSION['mailcow_cc_username'];
+
+ // check for empty user and role
+ if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
+
+ try {
+ if (!is_numeric($id)) $access_denied = true;
+
+ // set access_denied error
+ if ($access_denied){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ // check if it's last key
+ $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
+ WHERE `username` = :username AND `active` = '1'");
+ $stmt->execute(array(':username' => $username));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($row['keys'] == "1") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'last_key'
+ );
+ return false;
+ }
+
+ // delete key
+ $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
+ $stmt->execute(array(':username' => $username, ':id' => $id));
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('object_modified', $username)
+ );
+ }
+ catch (PDOException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => array('mysql_error', $e)
+ );
+ return false;
+ }
+}
+function get_tfa($username = null, $id = null) {
+ global $pdo;
+ if (isset($_SESSION['mailcow_cc_username'])) {
+ $username = $_SESSION['mailcow_cc_username'];
+ }
+ elseif (empty($username)) {
+ return false;
+ }
+
+ if (!isset($id)){
+ // fetch all tfa methods - just get information about possible authenticators
+ $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
+ WHERE `username` = :username AND `active` = '1'");
+ $stmt->execute(array(':username' => $username));
+ $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ // no tfa methods found
+ if (count($results) == 0) {
+ $data['name'] = 'none';
+ $data['pretty'] = "-";
+ $data['additional'] = array();
+ return $data;
+ }
+
+ $data['additional'] = $results;
+ return $data;
+ } else {
+ // fetch specific authenticator details by id
+ $stmt = $pdo->prepare("SELECT * FROM `tfa`
+ WHERE `username` = :username AND `id` = :id AND `active` = '1'");
+ $stmt->execute(array(':username' => $username, ':id' => $id));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (isset($row["authmech"])) {
+ switch ($row["authmech"]) {
+ case "yubi_otp":
+ $data['name'] = "yubi_otp";
+ $data['pretty'] = "Yubico OTP";
+ $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':id' => $id
+ ));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $data['additional'][] = $row;
+ }
+ return $data;
+ break;
+ // u2f - deprecated, should be removed
+ case "u2f":
+ $data['name'] = "u2f";
+ $data['pretty'] = "Fido U2F";
+ $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':id' => $id
+ ));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $data['additional'][] = $row;
+ }
+ return $data;
+ break;
+ case "hotp":
+ $data['name'] = "hotp";
+ $data['pretty'] = "HMAC-based OTP";
+ return $data;
+ break;
+ case "totp":
+ $data['name'] = "totp";
+ $data['pretty'] = "Time-based OTP";
+ $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':id' => $id
+ ));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $data['additional'][] = $row;
+ }
+ return $data;
+ break;
+ case "webauthn":
+ $data['name'] = "webauthn";
+ $data['pretty'] = "WebAuthn";
+ $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':id' => $id
+ ));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ while($row = array_shift($rows)) {
+ $data['additional'][] = $row;
+ }
+ return $data;
+ break;
+ default:
+ $data['name'] = 'none';
+ $data['pretty'] = "-";
+ return $data;
+ break;
+ }
+ }
+ else {
+ $data['name'] = 'none';
+ $data['pretty'] = "-";
+ return $data;
+ }
+ }
+}
+function verify_tfa_login($username, $_data) {
+ global $pdo;
+ global $yubi;
+ global $u2f;
+ global $tfa;
+ global $WebAuthn;
+
+ if ($_data['tfa_method'] != 'u2f'){
+
+ switch ($_data["tfa_method"]) {
+ case "yubi_otp":
+ if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('yotp_verification_failed', 'token length error')
+ );
+ return false;
+ }
+ $yubico_modhex_id = substr($_data['token'], 0, 12);
+ $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
+ WHERE `username` = :username
+ AND `authmech` = 'yubi_otp'
+ AND `active` = '1'
+ AND `secret` LIKE :modhex");
+ $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ $yubico_auth = explode(':', $row['secret']);
+ $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
+ $yauth = $yubi->verify($_data['token']);
+ if (PEAR::isError($yauth)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('yotp_verification_failed', $yauth->getMessage())
+ );
+ return false;
+ }
+ else {
+ $_SESSION['tfa_id'] = $row['id'];
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => 'verified_yotp_login'
+ );
+ return true;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('yotp_verification_failed', 'unknown')
+ );
+ return false;
+ break;
+ case "hotp":
+ return false;
+ break;
+ case "totp":
+ try {
+ $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
+ WHERE `username` = :username
+ AND `authmech` = 'totp'
+ AND `id` = :id
+ AND `active`='1'");
+ $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($rows as $row) {
+ if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
+ $_SESSION['tfa_id'] = $row['id'];
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => 'verified_totp_login'
+ );
+ return true;
+ }
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => 'totp_verification_failed'
+ );
+ return false;
+ }
+ catch (PDOException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('mysql_error', $e)
+ );
+ return false;
+ }
+ break;
+ case "webauthn":
+ $tokenData = json_decode($_data['token']);
+ $clientDataJSON = base64_decode($tokenData->clientDataJSON);
+ $authenticatorData = base64_decode($tokenData->authenticatorData);
+ $signature = base64_decode($tokenData->signature);
+ $id = base64_decode($tokenData->id);
+ $challenge = $_SESSION['challenge'];
+
+ $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
+ $stmt->execute(array(':id' => $_data['id']));
+ $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (empty($process_webauthn)){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('webauthn_verification_failed', 'authenticator not found')
+ );
+ return false;
+ }
+
+ if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('webauthn_verification_failed', 'publicKey not found')
+ );
+ return false;
+ }
+
+ try {
+ $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
+ }
+ catch (Throwable $ex) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('webauthn_verification_failed', $ex->getMessage())
+ );
+ return false;
+ }
+
+ $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
+ $stmt->execute(array(':username' => $process_webauthn['username']));
+ $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($obj_props['superadmin'] === 1) {
+ $_SESSION["mailcow_cc_role"] = "admin";
+ }
+ elseif ($obj_props['superadmin'] === 0) {
+ $_SESSION["mailcow_cc_role"] = "domainadmin";
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
+ $stmt->execute(array(':username' => $process_webauthn['username']));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (!empty($row['username'])) {
+ $_SESSION["mailcow_cc_role"] = "user";
+ } else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('webauthn_verification_failed', 'could not determine user role')
+ );
+ return false;
+ }
+ }
+
+ if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
+ );
+ return false;
+ }
+
+ $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
+ $_SESSION['tfa_id'] = $process_webauthn['id'];
+ $_SESSION['authReq'] = null;
+ unset($_SESSION["challenge"]);
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array("webauthn_login"),
+ 'msg' => array('logged_in_as', $process_webauthn['username'])
+ );
+ return true;
+ break;
+ default:
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $username, '*'),
+ 'msg' => 'unknown_tfa_method'
+ );
+ return false;
+ break;
+ }
+
+ return false;
+ } else {
+ // delete old keys that used u2f
+ $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if (count($rows) == 0) return false;
+
+ $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ return true;
+ }
+}
+function admin_api($access, $action, $data = null) {
+ global $pdo;
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ if ($access !== "ro" && $access !== "rw") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'invalid access type'
+ );
+ return false;
+ }
+ if ($action == "edit") {
+ $active = (!empty($data['active'])) ? 1 : 0;
+ $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
+ $allow_from = array();
+ if (isset($data['allow_from'])) {
+ $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
+ }
+ foreach ($allow_from as $key => $val) {
+ if (empty($val)) {
+ unset($allow_from[$key]);
+ continue;
+ }
+ if (valid_network($val) !== true) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'log' => array(__FUNCTION__, $data),
+ 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
+ );
+ unset($allow_from[$key]);
+ continue;
+ }
+ }
+ $allow_from = implode(',', array_unique(array_filter($allow_from)));
+ if (empty($allow_from) && $skip_ip_check == 0) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $data),
+ 'msg' => 'ip_list_empty'
+ );
+ return false;
+ }
+ $api_key = implode('-', array(
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3)))
+ ));
+ $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
+ $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+ if (empty($num_results)) {
+ $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
+ VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
+ $stmt->execute(array(
+ ':api_key' => $api_key,
+ ':skip_ip_check' => $skip_ip_check,
+ ':active' => $active,
+ ':allow_from' => $allow_from,
+ ':access' => $access
+ ));
+ }
+ else {
+ if ($skip_ip_check == 0) {
+ $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
+ `active` = :active,
+ `allow_from` = :allow_from
+ WHERE `access` = :access;");
+ $stmt->execute(array(
+ ':active' => $active,
+ ':skip_ip_check' => $skip_ip_check,
+ ':allow_from' => $allow_from,
+ ':access' => $access
+ ));
+ }
+ else {
+ $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
+ `active` = :active
+ WHERE `access` = :access;");
+ $stmt->execute(array(
+ ':active' => $active,
+ ':skip_ip_check' => $skip_ip_check,
+ ':access' => $access
+ ));
+ }
+ }
+ }
+ elseif ($action == "regen_key") {
+ $api_key = implode('-', array(
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3))),
+ strtoupper(bin2hex(random_bytes(3)))
+ ));
+ $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
+ $stmt->execute(array(
+ ':api_key' => $api_key,
+ ':access' => $access
+ ));
+ }
+ elseif ($action == "get") {
+ $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
+ $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($apidata !== false) {
+ $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
+ }
+ return $apidata;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $data),
+ 'msg' => 'admin_api_modified'
+ );
+}
+function license($action, $data = null) {
+ global $pdo;
+ global $redis;
+ global $lang;
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ switch ($action) {
+ case "verify":
+ // Keep result until revalidate button is pressed or session expired
+ $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
+ $versions = $stmt->fetch(PDO::FETCH_ASSOC);
+ $post = array('guid' => $versions['version']);
+ $curl = curl_init('https://verify.mailcow.email');
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
+ $response = curl_exec($curl);
+ curl_close($curl);
+ $json_return = json_decode($response, true);
+ if ($response && $json_return) {
+ if ($json_return['response'] === "ok") {
+ $_SESSION['gal']['valid'] = "true";
+ $_SESSION['gal']['c'] = $json_return['c'];
+ $_SESSION['gal']['s'] = $json_return['s'];
+ if ($json_return['m'] == 'NoMoore') {
+ $_SESSION['gal']['m'] = '🐄';
+ }
+ else {
+ $_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
+ }
+ }
+ elseif ($json_return['response'] === "invalid") {
+ $_SESSION['gal']['valid'] = "false";
+ $_SESSION['gal']['c'] = $lang['mailbox']['no'];
+ $_SESSION['gal']['s'] = $lang['mailbox']['no'];
+ $_SESSION['gal']['m'] = $lang['mailbox']['no'];
+ }
+ }
+ else {
+ $_SESSION['gal']['valid'] = "false";
+ $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
+ $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
+ $_SESSION['gal']['m'] = $lang['danger']['temp_error'];
+ }
+ try {
+ // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
+ $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ return $_SESSION['gal']['valid'];
+ break;
+ case "guid":
+ $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
+ $versions = $stmt->fetch(PDO::FETCH_ASSOC);
+ return $versions['version'];
+ break;
+ }
+}
+function rspamd_ui($action, $data = null) {
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ switch ($action) {
+ case "edit":
+ $rspamd_ui_pass = $data['rspamd_ui_pass'];
+ $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
+ if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, '*', '*'),
+ 'msg' => 'password_empty'
+ );
+ return false;
+ }
+ if ($rspamd_ui_pass != $rspamd_ui_pass2) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, '*', '*'),
+ 'msg' => 'password_mismatch'
+ );
+ return false;
+ }
+ if (strlen($rspamd_ui_pass) < 6) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, '*', '*'),
+ 'msg' => 'rspamd_ui_pw_length'
+ );
+ return false;
+ }
+ $docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
+ if ($docker_return_array = json_decode($docker_return, true)) {
+ if ($docker_return_array['type'] == 'success') {
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, '*', '*'),
+ 'msg' => 'rspamd_ui_pw_set'
+ );
+ return true;
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => $docker_return_array['type'],
+ 'log' => array(__FUNCTION__, '*', '*'),
+ 'msg' => $docker_return_array['msg']
+ );
+ return false;
+ }
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, '*', '*'),
+ 'msg' => 'unknown'
+ );
+ return false;
+ }
+ break;
+ }
+}
+
+function get_logs($application, $lines = false) {
+ if ($lines === false) {
+ $lines = $GLOBALS['LOG_LINES'] - 1;
+ }
+ elseif(is_numeric($lines) && $lines >= 1) {
+ $lines = abs(intval($lines) - 1);
+ }
+ else {
+ list ($from, $to) = explode('-', $lines);
+ $from = intval($from);
+ $to = intval($to);
+ if ($from < 1 || $to < $from) { return false; }
+ }
+ global $redis;
+ global $pdo;
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ return false;
+ }
+ // SQL
+ if ($application == "mailcow-ui") {
+ if (isset($from) && isset($to)) {
+ $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
+ $stmt->execute(array(
+ ':from' => $from - 1,
+ ':to' => $to
+ ));
+ $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
+ $stmt->execute(array(
+ ':lines' => $lines + 1,
+ ));
+ $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+ if (is_array($data)) {
+ return $data;
+ }
+ }
+ if ($application == "sasl") {
+ if (isset($from) && isset($to)) {
+ $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :from, :to");
+ $stmt->execute(array(
+ ':from' => $from - 1,
+ ':to' => $to
+ ));
+ $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+ else {
+ $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :lines");
+ $stmt->execute(array(
+ ':lines' => $lines + 1,
+ ));
+ $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+ if (is_array($data)) {
+ return $data;
+ }
+ }
+ // Redis
+ if ($application == "dovecot-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "postfix-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "sogo-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('SOGO_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "watchdog-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "acme-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('ACME_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "ratelimited") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('RL_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "api-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('API_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "netfilter-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "autodiscover-mailcow") {
+ if (isset($from) && isset($to)) {
+ $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
+ }
+ else {
+ $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
+ }
+ if ($data) {
+ foreach ($data as $json_line) {
+ $data_array[] = json_decode($json_line, true);
+ }
+ return $data_array;
+ }
+ }
+ if ($application == "rspamd-history") {
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
+ if (!is_numeric($lines)) {
+ list ($from, $to) = explode('-', $lines);
+ curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
+ }
+ else {
+ curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
+ }
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ $history = curl_exec($curl);
+ if (!curl_errno($curl)) {
+ $data_array = json_decode($history, true);
+ curl_close($curl);
+ return $data_array['rows'];
+ }
+ curl_close($curl);
+ return false;
+ }
+ if ($application == "rspamd-stats") {
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
+ curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ $stats = curl_exec($curl);
+ if (!curl_errno($curl)) {
+ $data_array = json_decode($stats, true);
+ curl_close($curl);
+ return $data_array;
+ }
+ curl_close($curl);
+ return false;
+ }
+ return false;
+}
+function getGUID() {
+ if (function_exists('com_create_guid')) {
+ return com_create_guid();
+ }
+ mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
+ $charid = strtoupper(md5(uniqid(rand(), true)));
+ $hyphen = chr(45);// "-"
+ return substr($charid, 0, 8).$hyphen
+ .substr($charid, 8, 4).$hyphen
+ .substr($charid,12, 4).$hyphen
+ .substr($charid,16, 4).$hyphen
+ .substr($charid,20,12);
+}
+function solr_status() {
+ $curl = curl_init();
+ $endpoint = 'http://solr:8983/solr/admin/cores';
+ $params = array(
+ 'action' => 'STATUS',
+ 'core' => 'dovecot-fts',
+ 'indexInfo' => 'true'
+ );
+ $url = $endpoint . '?' . http_build_query($params);
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_POST, 0);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 10);
+ $response_core = curl_exec($curl);
+ if ($response_core === false) {
+ $err = curl_error($curl);
+ curl_close($curl);
+ return false;
+ }
+ else {
+ curl_close($curl);
+ $curl = curl_init();
+ $status_core = json_decode($response_core, true);
+ $url = 'http://solr:8983/solr/admin/info/system';
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_POST, 0);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 10);
+ $response_sysinfo = curl_exec($curl);
+ if ($response_sysinfo === false) {
+ $err = curl_error($curl);
+ curl_close($curl);
+ return false;
+ }
+ else {
+ curl_close($curl);
+ $status_sysinfo = json_decode($response_sysinfo, true);
+ $status = array_merge($status_core, $status_sysinfo);
+ return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
+ }
+ return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
+ }
+ return false;
+}
+
+function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
+ $now = time();
+ foreach (glob($folder) as $filename) {
+ if(strpos($filename, $ignore) !== false) {
+ continue;
+ }
+ if (is_file($filename)) {
+ if ($now - filemtime($filename) >= 60 * 60) {
+ unlink($filename);
+ }
+ }
+ }
+}
+
+function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
+ $now = time();
+ foreach (glob($folder) as $filename) {
+ if(strpos($filename, $ignore) !== false) {
+ continue;
+ }
+ if (is_file($filename)) {
+ if ($now - filemtime($filename) >= 60 * 60) {
+ unlink($filename);
+ }
+ }
+ }
+}
+
+?>
diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.netfilter.inc.php
similarity index 76%
rename from data/web/inc/functions.fail2ban.inc.php
rename to data/web/inc/functions.netfilter.inc.php
index 2a7f11e8..bf4eb067 100644
--- a/data/web/inc/functions.fail2ban.inc.php
+++ b/data/web/inc/functions.netfilter.inc.php
@@ -1,329 +1,329 @@
-Get('F2B_OPTIONS'), true);
- $f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true);
- $wl = $redis->hGetAll('F2B_WHITELIST');
- if (is_array($wl)) {
- foreach ($wl as $key => $value) {
- $tmp_wl_data[] = $key;
- }
- if (isset($tmp_wl_data)) {
- natsort($tmp_wl_data);
- $f2b_options['whitelist'] = implode(PHP_EOL, (array)$tmp_wl_data);
- }
- else {
- $f2b_options['whitelist'] = "";
- }
- }
- else {
- $f2b_options['whitelist'] = "";
- }
- $bl = $redis->hGetAll('F2B_BLACKLIST');
- if (is_array($bl)) {
- foreach ($bl as $key => $value) {
- $tmp_bl_data[] = $key;
- }
- if (isset($tmp_bl_data)) {
- natsort($tmp_bl_data);
- $f2b_options['blacklist'] = implode(PHP_EOL, (array)$tmp_bl_data);
- }
- else {
- $f2b_options['blacklist'] = "";
- }
- }
- else {
- $f2b_options['blacklist'] = "";
- }
- $pb = $redis->hGetAll('F2B_PERM_BANS');
- if (is_array($pb)) {
- foreach ($pb as $key => $value) {
- $f2b_options['perm_bans'][] = array(
- 'network'=>$key,
- 'ip' => strtok($key,'/')
- );
-
- }
- }
- else {
- $f2b_options['perm_bans'] = "";
- }
- $active_bans = $redis->hGetAll('F2B_ACTIVE_BANS');
- $queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN');
- if (is_array($active_bans)) {
- foreach ($active_bans as $network => $banned_until) {
- $queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
- $difference = $banned_until - time();
- $f2b_options['active_bans'][] = array(
- 'queued_for_unban' => $queued_for_unban,
- 'network' => $network,
- 'ip' => strtok($network,'/'),
- 'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
- );
- }
- }
- else {
- $f2b_options['active_bans'] = "";
- }
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- return $f2b_options;
- break;
- case 'edit':
- if ($_SESSION['mailcow_cc_role'] != "admin") {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
- // Start to read actions, if any
- if (isset($_data['action'])) {
- // Reset regex filters
- if ($_data['action'] == "reset-regex") {
- try {
- $redis->Del('F2B_REGEX');
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- // Rules will also be recreated on log events, but rules may seem empty for a second in the UI
- docker('post', 'netfilter-mailcow', 'restart');
- $fail_count = 0;
- $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
- while (empty($regex_result) && $fail_count < 10) {
- $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
- $fail_count++;
- sleep(1);
- }
- if ($fail_count >= 10) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('reset_f2b_regex')
- );
- return false;
- }
- }
- elseif ($_data['action'] == "edit-regex") {
- if (!empty($_data['regex'])) {
- $rule_id = 1;
- $regex_array = array();
- foreach($_data['regex'] as $regex) {
- $regex_array[$rule_id] = $regex;
- $rule_id++;
- }
- if (!empty($regex_array)) {
- $redis->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
- }
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($network))
- );
- return true;
- }
-
- // Start actions in dependency of network
- if (!empty($_data['network'])) {
- $networks = (array)$_data['network'];
- foreach ($networks as $network) {
- // Unban network
- if ($_data['action'] == "unban") {
- if (valid_network($network)) {
- try {
- $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- continue;
- }
- }
- }
- // Whitelist network
- elseif ($_data['action'] == "whitelist") {
- if (empty($network)) { continue; }
- if (valid_network($network)) {
- try {
- $redis->hSet('F2B_WHITELIST', $network, 1);
- $redis->hDel('F2B_BLACKLIST', $network, 1);
- $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- continue;
- }
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('network_host_invalid', $network)
- );
- continue;
- }
- }
- // Blacklist network
- elseif ($_data['action'] == "blacklist") {
- if (empty($network)) { continue; }
- if (valid_network($network) && !in_array($network, array(
- '0.0.0.0',
- '0.0.0.0/0',
- getenv('IPV4_NETWORK') . '0/24',
- getenv('IPV4_NETWORK') . '0',
- getenv('IPV6_NETWORK')
- ))) {
- try {
- $redis->hSet('F2B_BLACKLIST', $network, 1);
- $redis->hDel('F2B_WHITELIST', $network, 1);
- //$response = docker('post', 'netfilter-mailcow', 'restart');
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- continue;
- }
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('network_host_invalid', $network)
- );
- continue;
- }
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($network))
- );
- }
- return true;
- }
- }
- // Start default edit without specific action
- $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['max_attempts']);
- $retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
- $netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
- $netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
- $wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
- $bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
- $f2b_options = array();
- $f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
- $f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
- $f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
- $f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
- $f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
- $f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
- $f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
- try {
- $redis->Set('F2B_OPTIONS', json_encode($f2b_options));
- $redis->Del('F2B_WHITELIST');
- $redis->Del('F2B_BLACKLIST');
- if(!empty($wl)) {
- $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
- $wl_array = array_filter($wl_array);
- if (is_array($wl_array)) {
- foreach ($wl_array as $wl_item) {
- if (valid_network($wl_item) || valid_hostname($wl_item)) {
- $redis->hSet('F2B_WHITELIST', $wl_item, 1);
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('network_host_invalid', $wl_item)
- );
- continue;
- }
- }
- }
- }
- if(!empty($bl)) {
- $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
- $bl_array = array_filter($bl_array);
- if (is_array($bl_array)) {
- foreach ($bl_array as $bl_item) {
- if (valid_network($bl_item) && !in_array($bl_item, array(
- '0.0.0.0',
- '0.0.0.0/0',
- getenv('IPV4_NETWORK') . '0/24',
- getenv('IPV4_NETWORK') . '0',
- getenv('IPV6_NETWORK')
- ))) {
- $redis->hSet('F2B_BLACKLIST', $bl_item, 1);
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('network_host_invalid', $bl_item)
- );
- continue;
- }
- }
- }
- }
- }
- catch (RedisException $e) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
- );
- return false;
- }
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => 'f2b_modified'
- );
- break;
- }
-}
+Get('NETFILTER_OPTIONS'), true);
+ $netfilter_options['regex'] = json_decode($redis->Get('NETFILTER_REGEX'), true);
+ $wl = $redis->hGetAll('NETFILTER_WHITELIST');
+ if (is_array($wl)) {
+ foreach ($wl as $key => $value) {
+ $tmp_wl_data[] = $key;
+ }
+ if (isset($tmp_wl_data)) {
+ natsort($tmp_wl_data);
+ $netfilter_options['whitelist'] = implode(PHP_EOL, (array)$tmp_wl_data);
+ }
+ else {
+ $netfilter_options['whitelist'] = "";
+ }
+ }
+ else {
+ $netfilter_options['whitelist'] = "";
+ }
+ $bl = $redis->hGetAll('NETFILTER_BLACKLIST');
+ if (is_array($bl)) {
+ foreach ($bl as $key => $value) {
+ $tmp_bl_data[] = $key;
+ }
+ if (isset($tmp_bl_data)) {
+ natsort($tmp_bl_data);
+ $netfilter_options['blacklist'] = implode(PHP_EOL, (array)$tmp_bl_data);
+ }
+ else {
+ $netfilter_options['blacklist'] = "";
+ }
+ }
+ else {
+ $netfilter_options['blacklist'] = "";
+ }
+ $pb = $redis->hGetAll('NETFILTER_PERM_BANS');
+ if (is_array($pb)) {
+ foreach ($pb as $key => $value) {
+ $netfilter_options['perm_bans'][] = array(
+ 'network'=>$key,
+ 'ip' => strtok($key,'/')
+ );
+
+ }
+ }
+ else {
+ $netfilter_options['perm_bans'] = "";
+ }
+ $active_bans = $redis->hGetAll('NETFILTER_ACTIVE_BANS');
+ $queue_unban = $redis->hGetAll('NETFILTER_QUEUE_UNBAN');
+ if (is_array($active_bans)) {
+ foreach ($active_bans as $network => $banned_until) {
+ $queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
+ $difference = $banned_until - time();
+ $netfilter_options['active_bans'][] = array(
+ 'queued_for_unban' => $queued_for_unban,
+ 'network' => $network,
+ 'ip' => strtok($network,'/'),
+ 'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
+ );
+ }
+ }
+ else {
+ $netfilter_options['active_bans'] = "";
+ }
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ return $netfilter_options;
+ break;
+ case 'edit':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ // Start to read actions, if any
+ if (isset($_data['action'])) {
+ // Reset regex filters
+ if ($_data['action'] == "reset-regex") {
+ try {
+ $redis->Del('NETFILTER_REGEX');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ // Rules will also be recreated on log events, but rules may seem empty for a second in the UI
+ docker('post', 'netfilter-mailcow', 'restart');
+ $fail_count = 0;
+ $regex_result = json_decode($redis->Get('NETFILTER_REGEX'), true);
+ while (empty($regex_result) && $fail_count < 10) {
+ $regex_result = json_decode($redis->Get('NETFILTER_REGEX'), true);
+ $fail_count++;
+ sleep(1);
+ }
+ if ($fail_count >= 10) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('reset_netfilter_regex')
+ );
+ return false;
+ }
+ }
+ elseif ($_data['action'] == "edit-regex") {
+ if (!empty($_data['regex'])) {
+ $rule_id = 1;
+ $regex_array = array();
+ foreach($_data['regex'] as $regex) {
+ $regex_array[$rule_id] = $regex;
+ $rule_id++;
+ }
+ if (!empty($regex_array)) {
+ $redis->Set('NETFILTER_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
+ }
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($network))
+ );
+ return true;
+ }
+
+ // Start actions in dependency of network
+ if (!empty($_data['network'])) {
+ $networks = (array)$_data['network'];
+ foreach ($networks as $network) {
+ // Unban network
+ if ($_data['action'] == "unban") {
+ if (valid_network($network)) {
+ try {
+ $redis->hSet('NETFILTER_QUEUE_UNBAN', $network, 1);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
+ }
+ }
+ // Whitelist network
+ elseif ($_data['action'] == "whitelist") {
+ if (empty($network)) { continue; }
+ if (valid_network($network)) {
+ try {
+ $redis->hSet('NETFILTER_WHITELIST', $network, 1);
+ $redis->hDel('NETFILTER_BLACKLIST', $network, 1);
+ $redis->hSet('NETFILTER_QUEUE_UNBAN', $network, 1);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $network)
+ );
+ continue;
+ }
+ }
+ // Blacklist network
+ elseif ($_data['action'] == "blacklist") {
+ if (empty($network)) { continue; }
+ if (valid_network($network) && !in_array($network, array(
+ '0.0.0.0',
+ '0.0.0.0/0',
+ getenv('IPV4_NETWORK') . '0/24',
+ getenv('IPV4_NETWORK') . '0',
+ getenv('IPV6_NETWORK')
+ ))) {
+ try {
+ $redis->hSet('NETFILTER_BLACKLIST', $network, 1);
+ $redis->hDel('NETFILTER_WHITELIST', $network, 1);
+ //$response = docker('post', 'netfilter-mailcow', 'restart');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $network)
+ );
+ continue;
+ }
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($network))
+ );
+ }
+ return true;
+ }
+ }
+ // Start default edit without specific action
+ $is_now = netfilter('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['max_attempts']);
+ $retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
+ $netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
+ $netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
+ $wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
+ $bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $netfilter_options = array();
+ $netfilter_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
+ $netfilter_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
+ $netfilter_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
+ $netfilter_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
+ $netfilter_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
+ $netfilter_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
+ $netfilter_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
+ try {
+ $redis->Set('NETFILTER_OPTIONS', json_encode($netfilter_options));
+ $redis->Del('NETFILTER_WHITELIST');
+ $redis->Del('NETFILTER_BLACKLIST');
+ if(!empty($wl)) {
+ $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
+ $wl_array = array_filter($wl_array);
+ if (is_array($wl_array)) {
+ foreach ($wl_array as $wl_item) {
+ if (valid_network($wl_item) || valid_hostname($wl_item)) {
+ $redis->hSet('NETFILTER_WHITELIST', $wl_item, 1);
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $wl_item)
+ );
+ continue;
+ }
+ }
+ }
+ }
+ if(!empty($bl)) {
+ $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
+ $bl_array = array_filter($bl_array);
+ if (is_array($bl_array)) {
+ foreach ($bl_array as $bl_item) {
+ if (valid_network($bl_item) && !in_array($bl_item, array(
+ '0.0.0.0',
+ '0.0.0.0/0',
+ getenv('IPV4_NETWORK') . '0/24',
+ getenv('IPV4_NETWORK') . '0',
+ getenv('IPV6_NETWORK')
+ ))) {
+ $redis->hSet('NETFILTER_BLACKLIST', $bl_item, 1);
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $bl_item)
+ );
+ continue;
+ }
+ }
+ }
+ }
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => 'netfilter_modified'
+ );
+ break;
+ }
+}
diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php
index b3b1cc13..581d1e45 100644
--- a/data/web/inc/prerequisites.inc.php
+++ b/data/web/inc/prerequisites.inc.php
@@ -274,7 +274,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.netfilter.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailq.inc.php';
@@ -317,7 +317,7 @@ $UI_TEXTS = customize('get', 'ui_texts');
if (file_exists('/web/css/themes/'.$UI_THEME.'-bootstrap.css'))
$css_minifier->add('/web/css/themes/'.$UI_THEME.'-bootstrap.css');
else
- $css_minifier->add('/web/css/themes/lumen-bootstrap.css');
+ $css_minifier->add('/web/css/themes/lumen-bootstrap.css');
// minify css build files
foreach ($css_dir as $css_file) {
$css_minifier->add('/web/css/build/' . $css_file);
diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php
index 5c7ec710..6c03b00e 100644
--- a/data/web/inc/sessions.inc.php
+++ b/data/web/inc/sessions.inc.php
@@ -1,140 +1,140 @@
- $SESSION_LIFETIME)) {
- session_unset();
- session_destroy();
-}
-$_SESSION['LAST_ACTIVITY'] = time();
-
-// API
-if (!empty($_SERVER['HTTP_X_API_KEY'])) {
- $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
- $stmt->execute(array(
- ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
- ));
- $api_return = $stmt->fetch(PDO::FETCH_ASSOC);
- if (!empty($api_return['api_key'])) {
- $skip_ip_check = ($api_return['skip_ip_check'] == 1);
- $remote = get_remote_ip(false);
- $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
- if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
- $_SESSION['mailcow_cc_username'] = 'API';
- $_SESSION['mailcow_cc_role'] = 'admin';
- $_SESSION['mailcow_cc_api'] = true;
- if ($api_return['access'] == 'rw') {
- $_SESSION['mailcow_cc_api_access'] = 'rw';
- }
- else {
- $_SESSION['mailcow_cc_api_access'] = 'ro';
- }
- }
- else {
- $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
- error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- http_response_code(401);
- echo json_encode(array(
- 'type' => 'error',
- 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
- ));
- unset($_POST);
- exit();
- }
- }
- else {
- $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
- error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
- http_response_code(401);
- echo json_encode(array(
- 'type' => 'error',
- 'msg' => 'authentication failed'
- ));
- unset($_POST);
- exit();
- }
-}
-
-// Handle logouts
-if (isset($_POST["logout"])) {
- if (isset($_SESSION["dual-login"])) {
- $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
- $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
- unset($_SESSION["dual-login"]);
- header("Location: /mailbox");
- exit();
- }
- else {
- session_regenerate_id(true);
- session_unset();
- session_destroy();
- session_write_close();
- header("Location: /");
- }
-}
-
-// Check session
-function session_check() {
- if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
- return true;
- }
- if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
- $_SESSION['return'][] = array(
- 'type' => 'warning',
- 'msg' => 'session_ua'
- );
- return false;
- }
- if (!empty($_POST)) {
- if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
- $_SESSION['return'][] = array(
- 'type' => 'warning',
- 'msg' => 'session_token'
- );
- return false;
- }
- unset($_POST['csrf_token']);
- $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
- $_SESSION['CSRF']['TIME'] = time();
- }
- return true;
-}
-
-if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
- $_POST = array();
- $_FILES = array();
-}
+ $SESSION_LIFETIME)) {
+ session_unset();
+ session_destroy();
+}
+$_SESSION['LAST_ACTIVITY'] = time();
+
+// API
+if (!empty($_SERVER['HTTP_X_API_KEY'])) {
+ $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
+ $stmt->execute(array(
+ ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
+ ));
+ $api_return = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (!empty($api_return['api_key'])) {
+ $skip_ip_check = ($api_return['skip_ip_check'] == 1);
+ $remote = get_remote_ip(false);
+ $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
+ if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
+ $_SESSION['mailcow_cc_username'] = 'API';
+ $_SESSION['mailcow_cc_role'] = 'admin';
+ $_SESSION['mailcow_cc_api'] = true;
+ if ($api_return['access'] == 'rw') {
+ $_SESSION['mailcow_cc_api_access'] = 'rw';
+ }
+ else {
+ $_SESSION['mailcow_cc_api_access'] = 'ro';
+ }
+ }
+ else {
+ $redis->publish("NETFILTER_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
+ error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ http_response_code(401);
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
+ ));
+ unset($_POST);
+ exit();
+ }
+ }
+ else {
+ $redis->publish("NETFILTER_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
+ error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+ http_response_code(401);
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'authentication failed'
+ ));
+ unset($_POST);
+ exit();
+ }
+}
+
+// Handle logouts
+if (isset($_POST["logout"])) {
+ if (isset($_SESSION["dual-login"])) {
+ $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
+ $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
+ unset($_SESSION["dual-login"]);
+ header("Location: /mailbox");
+ exit();
+ }
+ else {
+ session_regenerate_id(true);
+ session_unset();
+ session_destroy();
+ session_write_close();
+ header("Location: /");
+ }
+}
+
+// Check session
+function session_check() {
+ if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
+ return true;
+ }
+ if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'msg' => 'session_ua'
+ );
+ return false;
+ }
+ if (!empty($_POST)) {
+ if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
+ $_SESSION['return'][] = array(
+ 'type' => 'warning',
+ 'msg' => 'session_token'
+ );
+ return false;
+ }
+ unset($_POST['csrf_token']);
+ $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
+ $_SESSION['CSRF']['TIME'] = time();
+ }
+ return true;
+}
+
+if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
+ $_POST = array();
+ $_FILES = array();
+}
diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js
index 0dba1aa8..1050285e 100644
--- a/data/web/js/site/admin.js
+++ b/data/web/js/site/admin.js
@@ -26,7 +26,6 @@ jQuery(function($){
event.preventDefault();
var regex_map_id = $(this).data('regex-map');
var regex_data = $(jq(regex_map_id)).val().split(/\r?\n/);
- var regex_valid = true;
for(var i = 0;i < regex_data.length;i++){
if(regex_data[i].startsWith('#') || !regex_data[i]){
continue;
@@ -706,9 +705,9 @@ jQuery(function($){
cols = ' ';
cols += ' ';
cols += '' + lang.remove_row + ' ';
- } else if (type == "f2b_regex") {
- cols = ' ';
- cols += ' ';
+ } else if (type == "netfilter_regex") {
+ cols = ' ';
+ cols += ' ';
cols += '' + lang.remove_row + ' ';
}
row.append(cols);
@@ -718,14 +717,14 @@ jQuery(function($){
e.preventDefault();
$(this).parents('tr').remove();
});
- $('#f2b_regex_table').on('click', 'tr a', function (e) {
+ $('#netfilter_regex_table').on('click', 'tr a', function (e) {
e.preventDefault();
$(this).parents('tr').remove();
});
$('#add_app_link_row').click(function() {
add_table_row($('#app_link_table'), "app_link");
});
- $('#add_f2b_regex_row').click(function() {
- add_table_row($('#f2b_regex_table'), "f2b_regex");
+ $('#add_netfilter_regex_row').click(function() {
+ add_table_row($('#netfilter_regex_table'), "netfilter_regex");
});
});
diff --git a/data/web/json_api.php b/data/web/json_api.php
index acaeaba9..52b1649b 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -1310,10 +1310,10 @@ if (isset($_GET['query'])) {
break;
}
break;
- case "fail2ban":
+ case "netfilter":
switch ($object) {
default:
- $data = fail2ban('get');
+ $data = netfilter('get');
process_get_return($data);
break;
}
@@ -1917,8 +1917,8 @@ if (isset($_GET['query'])) {
case "fwdhost":
process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
break;
- case "fail2ban":
- process_edit_return(fail2ban('edit', array_merge(array('network' => $items), $attr)));
+ case "netfilter":
+ process_edit_return(netfilter('edit', array_merge(array('network' => $items), $attr)));
break;
case "ui_texts":
process_edit_return(customize('edit', 'ui_texts', $attr));
diff --git a/data/web/lang/lang.ca-es.json b/data/web/lang/lang.ca-es.json
index 26a30afd..469600b6 100644
--- a/data/web/lang/lang.ca-es.json
+++ b/data/web/lang/lang.ca-es.json
@@ -90,13 +90,13 @@
"domain_admins": "Administradores de dominio",
"edit": "Editar",
"empty": "Sense resultats",
- "f2b_ban_time": "Temsp de bloqueig (s)",
- "f2b_max_attempts": "Intents màx.",
- "f2b_netban_ipv4": "Suxarxa IPv4 on aplicar el bloqueig (8-32)",
- "f2b_netban_ipv6": "Suxarxa IPv6 on aplicar el bloqueig (8-128)",
- "f2b_parameters": "Fail2ban",
- "f2b_retry_window": "Finestra de reintent (s) per intents màx.",
- "f2b_whitelist": "Llista blanca de xarxes/hosts",
+ "netfilter_ban_time": "Temsp de bloqueig (s)",
+ "netfilter_max_attempts": "Intents màx.",
+ "netfilter_netban_ipv4": "Suxarxa IPv4 on aplicar el bloqueig (8-32)",
+ "netfilter_netban_ipv6": "Suxarxa IPv6 on aplicar el bloqueig (8-128)",
+ "netfilter_parameters": "Netfilter",
+ "netfilter_retry_window": "Finestra de reintent (s) per intents màx.",
+ "netfilter_whitelist": "Llista blanca de xarxes/hosts",
"filter_table": "Filtrar taula",
"forwarding_hosts": "Forwarding Hosts",
"forwarding_hosts_add_hint": "Podeu especificar adreces IPv4/IPv6, xarxes en notació CIDR, noms de host (que es resoldran a adreces IP) o noms de domini (que es resoldran a les adreces IP consultant els registres SPF o, si no n'hi ha, registres MX ).",
@@ -418,7 +418,7 @@
"domain_modified": "S'han desat els canvis fets al domini %s",
"domain_removed": "S'ha elminat el domini %s",
"eas_reset": "S'ha fet un reset als dispositius ActiveSync de l'usuari %s",
- "f2b_modified": "S'han desat els canvis fets als parametres del Fail2ban",
+ "netfilter_modified": "S'han desat els canvis fets als parametres del Netfilter",
"forwarding_host_added": "Forwarding host %s afegit",
"forwarding_host_removed": "Forwarding host %s esborrat",
"item_deleted": "S'ha esborrat %s",
diff --git a/data/web/lang/lang.cs-cz.json b/data/web/lang/lang.cs-cz.json
index d1f2bb07..67ebe3d9 100644
--- a/data/web/lang/lang.cs-cz.json
+++ b/data/web/lang/lang.cs-cz.json
@@ -174,17 +174,17 @@
"edit": "Upravit",
"empty": "Žádné výsledky",
"excludes": "Vyloučit tyto příjemce",
- "f2b_ban_time": "Doba blokování (s)",
- "f2b_blacklist": "Sítě/hostitelé na blacklistu",
- "f2b_filter": "Regex filtre",
- "f2b_list_info": "Síť nebo hostitelé na blacklistu mají vždy větší váhu než položky na whitelistu. Blacklist se sestavuje vždy při startu kontejneru.",
- "f2b_max_attempts": "Max. pokusů",
- "f2b_netban_ipv4": "Rozsah IPv4 podsítě k zablokování (8-32)",
- "f2b_netban_ipv6": "Rozsah IPv6 podsítě k zablokování (8-128)",
- "f2b_parameters": "Parametry automatického firewallu",
- "f2b_regex_info": "Záznamy které se berou v úvahu: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Časový horizont pro maximum pokusů (s)",
- "f2b_whitelist": "Sítě/hostitelé na whitelistu",
+ "netfilter_ban_time": "Doba blokování (s)",
+ "netfilter_blacklist": "Sítě/hostitelé na blacklistu",
+ "netfilter_filter": "Regex filtre",
+ "netfilter_list_info": "Síť nebo hostitelé na blacklistu mají vždy větší váhu než položky na whitelistu. Blacklist se sestavuje vždy při startu kontejneru.",
+ "netfilter_max_attempts": "Max. pokusů",
+ "netfilter_netban_ipv4": "Rozsah IPv4 podsítě k zablokování (8-32)",
+ "netfilter_netban_ipv6": "Rozsah IPv6 podsítě k zablokování (8-128)",
+ "netfilter_parameters": "Parametry automatického firewallu",
+ "netfilter_regex_info": "Záznamy které se berou v úvahu: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Časový horizont pro maximum pokusů (s)",
+ "netfilter_whitelist": "Sítě/hostitelé na whitelistu",
"filter_table": "Tabulka filtrů",
"forwarding_hosts": "Předávající servery",
"forwarding_hosts_add_hint": "Lze zadat IPv4/IPv6 adresy, sítě ve formátu CIDR, názvy serverů (budou převedeny na IP adresy) nebo názvy domén (budou převedeny na IP pomocí SPF záznamů, příp. MX záznamů).",
@@ -423,7 +423,7 @@
"redis_error": "Chyba Redis: %s",
"relayhost_invalid": "Položky %s je neplatná",
"release_send_failed": "Zprávu nelze propustit: %s",
- "reset_f2b_regex": "Regex filtr se nepodařilo resetovat, zkuste to znovu nebo počkejte pár sekund a obnovte stránku.",
+ "reset_netfilter_regex": "Regex filtr se nepodařilo resetovat, zkuste to znovu nebo počkejte pár sekund a obnovte stránku.",
"resource_invalid": "Název zdroje %s je neplatný",
"rl_timeframe": "Nesprávný časový rámec omezení provozu",
"rspamd_ui_pw_length": "Heslo pro Rspamd UI musí mít alespoň 6 znaků",
@@ -938,7 +938,7 @@
"domain_removed": "Doména %s odebrána",
"dovecot_restart_success": "Dovecot byl úspěšně restartován",
"eas_reset": "ActiveSync zařízení uživatele %s vyresetováno",
- "f2b_modified": "Změny v parametrech automatického firewallu uloženy",
+ "netfilter_modified": "Změny v parametrech automatického firewallu uloženy",
"forwarding_host_added": "Předávající hostitel %s přidán",
"forwarding_host_removed": "Předávající hostitel %s odebrán",
"global_filter_written": "Filtr byl úspěšně zapsán",
diff --git a/data/web/lang/lang.da-dk.json b/data/web/lang/lang.da-dk.json
index c0d42afe..8de21a48 100644
--- a/data/web/lang/lang.da-dk.json
+++ b/data/web/lang/lang.da-dk.json
@@ -165,17 +165,17 @@
"edit": "Redigere",
"empty": "Ingen resultater",
"excludes": "Ekskluderer disse modtagere",
- "f2b_ban_time": "Udelukkelses tid (s)",
- "f2b_blacklist": "Sortlistede netværk/værter",
- "f2b_filter": "Regex filtre",
- "f2b_list_info": "En sortlistet vært eller et netværk opvejer altid en hvidlisted eksistens. Listeopdateringer tager nogle få sekunder at blive anvendt. ",
- "f2b_max_attempts": "Max. forsøg",
- "f2b_netban_ipv4": "IPv4 subnet størrelse at anvende forbud mod (8-32)",
- "f2b_netban_ipv6": "IPv6 subnet størrelse at anvende forbud mod (8-128)",
- "f2b_parameters": "Fail2ban parametre",
- "f2b_regex_info": "Logfiler taget i betragtning: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Genindlæs vindue om (s) for max. forsøg",
- "f2b_whitelist": "Hvidlisted netværk/vært",
+ "netfilter_ban_time": "Udelukkelses tid (s)",
+ "netfilter_blacklist": "Sortlistede netværk/værter",
+ "netfilter_filter": "Regex filtre",
+ "netfilter_list_info": "En sortlistet vært eller et netværk opvejer altid en hvidlisted eksistens. Listeopdateringer tager nogle få sekunder at blive anvendt. ",
+ "netfilter_max_attempts": "Max. forsøg",
+ "netfilter_netban_ipv4": "IPv4 subnet størrelse at anvende forbud mod (8-32)",
+ "netfilter_netban_ipv6": "IPv6 subnet størrelse at anvende forbud mod (8-128)",
+ "netfilter_parameters": "Netfilter parametre",
+ "netfilter_regex_info": "Logfiler taget i betragtning: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Genindlæs vindue om (s) for max. forsøg",
+ "netfilter_whitelist": "Hvidlisted netværk/vært",
"filter_table": "Filtertabel",
"forwarding_hosts": "Videresendelse af værter",
"forwarding_hosts_add_hint": "Du kan enten angive IPv4 / IPv6-adresser, netværk i CIDR-notation, værtsnavne (som løses til IP-adresser) eller domænenavne (som løses til IP-adresser ved at spørge SPF-poster eller i mangel af MX-poster).",
@@ -395,7 +395,7 @@
"redis_error": "Redis fejl: %s",
"relayhost_invalid": "Kortindtastning %s er ugyldig",
"release_send_failed": "Beskeden kunne ikke frigives: %s",
- "reset_f2b_regex": "Regex filter kunne ikke nulstilles i tide, prøv igen eller vent et par sekunder mere, og genindlæs webstedet.",
+ "reset_netfilter_regex": "Regex filter kunne ikke nulstilles i tide, prøv igen eller vent et par sekunder mere, og genindlæs webstedet.",
"resource_invalid": "Ressource navn %s er ugyldig",
"rl_timeframe": "Tidsramme for satsbegrænsning er forkert",
"rspamd_ui_pw_length": "Rspamd UI adgangskoden skal være mindst 6 tegn lang",
@@ -845,7 +845,7 @@
"domain_removed": "Domæne %s er blevet fjernet",
"dovecot_restart_success": "Dovecot blev genstartet",
"eas_reset": "ActiveSync-enheder til bruger %s blev nulstillet",
- "f2b_modified": "Changes to Fail2ban parameters have been saved",
+ "netfilter_modified": "Changes to Netfilter parameters have been saved",
"forwarding_host_added": "Videresendende vært %s er tilføjet",
"forwarding_host_removed": "Videresendende vært %s er blevet fjernet",
"global_filter_written": "Filtret blev skrevet til filen",
diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json
index 4f7feb5b..744cc1f7 100644
--- a/data/web/lang/lang.de-de.json
+++ b/data/web/lang/lang.de-de.json
@@ -174,17 +174,17 @@
"edit": "Bearbeiten",
"empty": "Keine Einträge vorhanden",
"excludes": "Diese Empfänger ausschließen",
- "f2b_ban_time": "Bannzeit in Sekunden",
- "f2b_blacklist": "Blacklist für Netzwerke und Hosts",
- "f2b_filter": "Regex-Filter",
- "f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. Die Aktualisierung der Liste dauert einige Sekunden. ",
- "f2b_max_attempts": "Max. Versuche",
- "f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
- "f2b_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)",
- "f2b_parameters": "Fail2ban-Parameter",
- "f2b_regex_info": "Berücksichtigte Logs: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Wiederholungen im Zeitraum von (s)",
- "f2b_whitelist": "Whitelist für Netzwerke und Hosts",
+ "netfilter_ban_time": "Bannzeit in Sekunden",
+ "netfilter_blacklist": "Blacklist für Netzwerke und Hosts",
+ "netfilter_filter": "Regex-Filter",
+ "netfilter_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. Die Aktualisierung der Liste dauert einige Sekunden. ",
+ "netfilter_max_attempts": "Max. Versuche",
+ "netfilter_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
+ "netfilter_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)",
+ "netfilter_parameters": "Netfilter-Parameter",
+ "netfilter_regex_info": "Berücksichtigte Logs: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Wiederholungen im Zeitraum von (s)",
+ "netfilter_whitelist": "Whitelist für Netzwerke und Hosts",
"filter_table": "Tabelle filtern",
"forwarding_hosts": "Weiterleitungs-Hosts",
"forwarding_hosts_add_hint": "Sie können entweder IPv4-/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.",
@@ -431,7 +431,7 @@
"redis_error": "Redis Fehler: %s",
"relayhost_invalid": "Map-Eintrag %s ist ungültig",
"release_send_failed": "Die Nachricht konnte nicht versendet werden: %s",
- "reset_f2b_regex": "Regex-Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.",
+ "reset_netfilter_regex": "Regex-Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.",
"resource_invalid": "Ressourcenname %s ist ungültig",
"rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt",
"rspamd_ui_pw_length": "Rspamd UI-Passwort muss mindestens 6 Zeichen lang sein",
@@ -1002,7 +1002,7 @@
"domain_removed": "Domain %s wurde entfernt",
"dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet",
"eas_reset": "ActiveSync Gerät des Benutzers %s wurde zurückgesetzt",
- "f2b_modified": "Änderungen an Fail2ban-Parametern wurden gespeichert",
+ "netfilter_modified": "Änderungen an Netfilter-Parametern wurden gespeichert",
"forwarding_host_added": "Weiterleitungs-Host %s wurde hinzugefügt",
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
"global_filter_written": "Filterdatei wurde erfolgreich geschrieben",
diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json
index 5719049e..fa490cf8 100644
--- a/data/web/lang/lang.en-gb.json
+++ b/data/web/lang/lang.en-gb.json
@@ -176,17 +176,17 @@
"edit": "Edit",
"empty": "No results",
"excludes": "Excludes these recipients",
- "f2b_ban_time": "Ban time (s)",
- "f2b_blacklist": "Blacklisted networks/hosts",
- "f2b_filter": "Regex filters",
- "f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. List updates will take a few seconds to be applied. ",
- "f2b_max_attempts": "Max. attempts",
- "f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
- "f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
- "f2b_parameters": "Fail2ban parameters",
- "f2b_regex_info": "Logs taken into consideration: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Retry window (s) for max. attempts",
- "f2b_whitelist": "Whitelisted networks/hosts",
+ "netfilter_ban_time": "Ban time (s)",
+ "netfilter_blacklist": "Blacklisted networks/hosts",
+ "netfilter_filter": "Regex filters",
+ "netfilter_list_info": "A blacklisted host or network will always outweigh a whitelist entity. List updates will take a few seconds to be applied. ",
+ "netfilter_max_attempts": "Max. attempts",
+ "netfilter_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
+ "netfilter_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
+ "netfilter_parameters": "Netfilter parameters",
+ "netfilter_regex_info": "Logs taken into consideration: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Retry window (s) for max. attempts",
+ "netfilter_whitelist": "Whitelisted networks/hosts",
"filter_table": "Filter table",
"forwarding_hosts": "Forwarding Hosts",
"forwarding_hosts_add_hint": "You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).",
@@ -432,7 +432,7 @@
"redis_error": "Redis error: %s",
"relayhost_invalid": "Map entry %s is invalid",
"release_send_failed": "Message could not be released: %s",
- "reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
+ "reset_netfilter_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
"resource_invalid": "Resource name %s is invalid",
"rl_timeframe": "Rate limit time frame is incorrect",
"rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long",
@@ -1015,7 +1015,7 @@
"domain_removed": "Domain %s has been removed",
"dovecot_restart_success": "Dovecot was restarted successfully",
"eas_reset": "ActiveSync devices for user %s were reset",
- "f2b_modified": "Changes to Fail2ban parameters have been saved",
+ "netfilter_modified": "Changes to Netfilter parameters have been saved",
"forwarding_host_added": "Forwarding host %s has been added",
"forwarding_host_removed": "Forwarding host %s has been removed",
"global_filter_written": "Filter was successfully written to file",
diff --git a/data/web/lang/lang.es-es.json b/data/web/lang/lang.es-es.json
index d9c3bfd3..8928b9d7 100644
--- a/data/web/lang/lang.es-es.json
+++ b/data/web/lang/lang.es-es.json
@@ -140,15 +140,15 @@
"edit": "Editar",
"empty": "Sin resultados",
"excludes": "Excluye a estos destinatarios",
- "f2b_ban_time": "Tiempo de restricción (s)",
- "f2b_blacklist": "Redes y hosts en lista negra",
- "f2b_list_info": "Un host o red en lista negra siempre superará a una entidad de la lista blanca. Las actualizaciones de la lista tardarán unos segundos en aplicarse. ",
- "f2b_max_attempts": "Max num. de intentos",
- "f2b_netban_ipv4": "Tamaño de subred IPv4 para aplicar la restricción (8-32)",
- "f2b_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)",
- "f2b_parameters": "Parametros Fail2ban",
- "f2b_retry_window": "Ventana de tiempo entre reintentos",
- "f2b_whitelist": "Redes y hosts en lista blanca",
+ "netfilter_ban_time": "Tiempo de restricción (s)",
+ "netfilter_blacklist": "Redes y hosts en lista negra",
+ "netfilter_list_info": "Un host o red en lista negra siempre superará a una entidad de la lista blanca. Las actualizaciones de la lista tardarán unos segundos en aplicarse. ",
+ "netfilter_max_attempts": "Max num. de intentos",
+ "netfilter_netban_ipv4": "Tamaño de subred IPv4 para aplicar la restricción (8-32)",
+ "netfilter_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)",
+ "netfilter_parameters": "Parametros Netfilter",
+ "netfilter_retry_window": "Ventana de tiempo entre reintentos",
+ "netfilter_whitelist": "Redes y hosts en lista blanca",
"filter_table": "Filtrar tabla",
"forwarding_hosts": "Hosts de reenvío",
"forwarding_hosts_add_hint": "Se puede especificar direcciones IPv4 / IPv6, redes en notación CIDR, nombres de host (que se resolverán en direcciones IP) o dominios (que se resolverán en direcciones IP consultando registros SPF o, en su defecto, registros MX)",
diff --git a/data/web/lang/lang.fi-fi.json b/data/web/lang/lang.fi-fi.json
index 7305e19e..9f444aad 100644
--- a/data/web/lang/lang.fi-fi.json
+++ b/data/web/lang/lang.fi-fi.json
@@ -149,15 +149,15 @@
"edit": "Muokata",
"empty": "Ei tuloksia",
"excludes": "Sulkee pois nämä vastaanottajat",
- "f2b_ban_time": "Ban aika (s)",
- "f2b_blacklist": "Mustalla listalla verkot/isännät",
- "f2b_list_info": "Mustalla listalla oleva isäntä tai verkko on aina suurempi kuin sallitut-entiteetti. Luettelon päivitykset otetaan käyttöön muutaman sekunnin kuluttua. ",
- "f2b_max_attempts": "Maksmi. Yritykset",
- "f2b_netban_ipv4": "IPv4 aliverkon koko kiellon soveltamiseksi (8-32)",
- "f2b_netban_ipv6": "IPv6 aliverkon koko kiellon soveltamiseksi (8-128)",
- "f2b_parameters": "Fail2ban parametrit",
- "f2b_retry_window": "Yritä uudelleen-ikkuna (s) Max. Yrittää",
- "f2b_whitelist": "Sallitut verkot/isännät",
+ "netfilter_ban_time": "Ban aika (s)",
+ "netfilter_blacklist": "Mustalla listalla verkot/isännät",
+ "netfilter_list_info": "Mustalla listalla oleva isäntä tai verkko on aina suurempi kuin sallitut-entiteetti. Luettelon päivitykset otetaan käyttöön muutaman sekunnin kuluttua. ",
+ "netfilter_max_attempts": "Maksmi. Yritykset",
+ "netfilter_netban_ipv4": "IPv4 aliverkon koko kiellon soveltamiseksi (8-32)",
+ "netfilter_netban_ipv6": "IPv6 aliverkon koko kiellon soveltamiseksi (8-128)",
+ "netfilter_parameters": "Netfilter parametrit",
+ "netfilter_retry_window": "Yritä uudelleen-ikkuna (s) Max. Yrittää",
+ "netfilter_whitelist": "Sallitut verkot/isännät",
"filter_table": "Suodata taulukko",
"forwarding_hosts": "Palveluntarjoajien välittäminen",
"forwarding_hosts_add_hint": "Voit joko määrittää IPv4 / IPv6-osoitteet, verkot CIDR-merkinnässä, isäntänimet (jotka määritetään IP-osoitteiksi) tai verkkotunnusten nimet (jotka määritetään IP-osoitteiksi kyselyllä SPF-tietueista tai niiden puuttuessa MX-tietueista). .",
@@ -725,7 +725,7 @@
"domain_modified": "Muutokset verkkotunnus alueeseen %s on tallennettu",
"domain_removed": "Verkkotunnus %s on poistettu",
"eas_reset": "ActiveSync-laitteet käyttäjälle %s nollataan",
- "f2b_modified": "Fail2ban parametrien muutokset on tallennettu",
+ "netfilter_modified": "Netfilter parametrien muutokset on tallennettu",
"forwarding_host_added": "Välitys host %s on lisätty",
"forwarding_host_removed": "Välitys host %s on poistettu",
"hash_deleted": "Hash poistettu",
diff --git a/data/web/lang/lang.fr-fr.json b/data/web/lang/lang.fr-fr.json
index 8afc5738..86d55904 100644
--- a/data/web/lang/lang.fr-fr.json
+++ b/data/web/lang/lang.fr-fr.json
@@ -171,17 +171,17 @@
"edit": "Editer",
"empty": "Aucun résultat",
"excludes": "Exclure ces destinataires",
- "f2b_ban_time": "Durée du bannissement(s)",
- "f2b_blacklist": "Réseaux/Domaines sur Liste Noire",
- "f2b_filter": "Filtre(s) Regex",
- "f2b_list_info": "Un hôte ou un réseau sur liste noire l'emportera toujours sur une entité de liste blanche. L'application des mises à jour de liste prendra quelques secondes. ",
- "f2b_max_attempts": "Nb max. de tentatives",
- "f2b_netban_ipv4": "Taille du sous-réseau IPv4 pour l'application du bannissement (8-32)",
- "f2b_netban_ipv6": "Taille du sous-réseau IPv6 pour l'application du bannissement (8-128)",
- "f2b_parameters": "Paramètres Fail2ban",
- "f2b_regex_info": "Logs pris en compte : SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Fenêtre de nouvel essai pour le nb max. de tentatives",
- "f2b_whitelist": "Réseaux/hôtes en liste blanche",
+ "netfilter_ban_time": "Durée du bannissement(s)",
+ "netfilter_blacklist": "Réseaux/Domaines sur Liste Noire",
+ "netfilter_filter": "Filtre(s) Regex",
+ "netfilter_list_info": "Un hôte ou un réseau sur liste noire l'emportera toujours sur une entité de liste blanche. L'application des mises à jour de liste prendra quelques secondes. ",
+ "netfilter_max_attempts": "Nb max. de tentatives",
+ "netfilter_netban_ipv4": "Taille du sous-réseau IPv4 pour l'application du bannissement (8-32)",
+ "netfilter_netban_ipv6": "Taille du sous-réseau IPv6 pour l'application du bannissement (8-128)",
+ "netfilter_parameters": "Paramètres Netfilter",
+ "netfilter_regex_info": "Logs pris en compte : SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Fenêtre de nouvel essai pour le nb max. de tentatives",
+ "netfilter_whitelist": "Réseaux/hôtes en liste blanche",
"filter_table": "Table de filtrage",
"forwarding_hosts": "Hôtes de réexpédition",
"forwarding_hosts_add_hint": "Vous pouvez aussi bien indiquer des adresses IPv4/IPv6, des réseaux en notation CIDR, des noms d'hôtes (qui seront convertis en adresses IP), ou des noms de domaine (qui seront convertis en adresses IP par une requête SPF ou, en son absence, l'enregistrement MX).",
@@ -410,7 +410,7 @@
"redis_error": "Erreur Redis : %s",
"relayhost_invalid": "La saisie de la carte %s est invalide",
"release_send_failed": "Le message n’a pas pu être diffusé : %s",
- "reset_f2b_regex": "Le filtre regex n'a pas pu être réinitialisé à temps, veuillez réessayer ou attendre quelques secondes de plus et recharger le site web.",
+ "reset_netfilter_regex": "Le filtre regex n'a pas pu être réinitialisé à temps, veuillez réessayer ou attendre quelques secondes de plus et recharger le site web.",
"resource_invalid": "Le nom de la resource %s n'est pas valide",
"rl_timeframe": "Le délai limite du taux est incorrect",
"rspamd_ui_pw_length": "Le mot de passe de l'interface Rspamd doit être de 6 caratères au minimum",
@@ -874,7 +874,7 @@
"domain_removed": "Le domaine %s a été supprimé",
"dovecot_restart_success": "Dovecot a été relancé avec succès",
"eas_reset": "Les périphériques Activesync pour l’utilisateur %s ont été réinitialisés",
- "f2b_modified": "Les modifications apportées aux paramètres Fail2ban ont été enregistrées",
+ "netfilter_modified": "Les modifications apportées aux paramètres Netfilter ont été enregistrées",
"forwarding_host_added": "Ajout de l’hôte de réacheminement %s",
"forwarding_host_removed": "Suppression de l’hôte de réacheminement %s",
"global_filter_written": "Le filtre a été écrit avec succès dans le fichier",
diff --git a/data/web/lang/lang.it-it.json b/data/web/lang/lang.it-it.json
index 8bfa9738..83577e40 100644
--- a/data/web/lang/lang.it-it.json
+++ b/data/web/lang/lang.it-it.json
@@ -174,17 +174,17 @@
"edit": "Modifica",
"empty": "Nessun risultato",
"excludes": "Esclude questi destinatari",
- "f2b_ban_time": "Tempo di blocco (s)",
- "f2b_blacklist": "Host/reti in blacklist",
- "f2b_filter": "Filtri Regex",
- "f2b_list_info": "Un host oppure una rete in blacklist, avrà sempre un peso maggiore rispetto ad una in whitelist. L'aggiornamento della lista richiede alcuni secondi per la sua entrata in azione. ",
- "f2b_max_attempts": "Tentativi massimi",
- "f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
- "f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
- "f2b_parameters": "Parametri Fail2ban",
- "f2b_regex_info": "Log presi in considerazione: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Retry window (s) for max. attempts",
- "f2b_whitelist": "Host/reti in whitelist",
+ "netfilter_ban_time": "Tempo di blocco (s)",
+ "netfilter_blacklist": "Host/reti in blacklist",
+ "netfilter_filter": "Filtri Regex",
+ "netfilter_list_info": "Un host oppure una rete in blacklist, avrà sempre un peso maggiore rispetto ad una in whitelist. L'aggiornamento della lista richiede alcuni secondi per la sua entrata in azione. ",
+ "netfilter_max_attempts": "Tentativi massimi",
+ "netfilter_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
+ "netfilter_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
+ "netfilter_parameters": "Parametri Netfilter",
+ "netfilter_regex_info": "Log presi in considerazione: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Retry window (s) for max. attempts",
+ "netfilter_whitelist": "Host/reti in whitelist",
"filter_table": "Tabella filtro",
"forwarding_hosts": "Inoltro degli host",
"forwarding_hosts_add_hint": "È possibile specificare indirizzi IPv4 / IPv6, reti nella notazione CIDR, nomi host (che verranno risolti in indirizzi IP) o nomi di dominio (che verranno risolti agli indirizzi IP richiamando i record SPF o, in assenza, i record MX) .",
@@ -426,7 +426,7 @@
"redis_error": "Redis error: %s",
"relayhost_invalid": "Map entry %s is invalid",
"release_send_failed": "Message could not be released: %s",
- "reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
+ "reset_netfilter_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
"resource_invalid": "Il nome della risorsa non è valido",
"rl_timeframe": "Rate limit time frame is incorrect",
"rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long",
@@ -972,7 +972,7 @@
"domain_removed": "Il dominio %s è stato aggiunto",
"dovecot_restart_success": "Dovecot è stato riavviato con successo",
"eas_reset": "I dispositivi ActiveSync dell'utente %s sono stati resettati",
- "f2b_modified": "Le modifiche ai parametri Fail2ban sono state salvate",
+ "netfilter_modified": "Le modifiche ai parametri Netfilter sono state salvate",
"forwarding_host_added": "Inoltro dell' host %s è stato aggiunto",
"forwarding_host_removed": "Inoltro dell' host %s è stato rimosso",
"global_filter_written": "Il filtro è stato scritto con successo nel file",
diff --git a/data/web/lang/lang.ko-kr.json b/data/web/lang/lang.ko-kr.json
index 3f3cb153..d1e36562 100644
--- a/data/web/lang/lang.ko-kr.json
+++ b/data/web/lang/lang.ko-kr.json
@@ -163,15 +163,15 @@
"edit": "편집",
"empty": "값이 없음",
"excludes": "다음 수신자 제외",
- "f2b_ban_time": "차단 시간 (s)",
- "f2b_blacklist": "블랙리스트에 기록된 네트워크/호스트",
- "f2b_list_info": "블랙리스트에 기록된 호스트 혹은 네트워크는 항상 화이트 리스트보다 먼저 고려됩니다. 리스트를 업데이트하면 적용되는데 시간이 조금 걸릴 수 있습니다. ",
- "f2b_max_attempts": "최대 시도 횟수",
- "f2b_netban_ipv4": "차단할 IPv4 서브넷 크기 (8-32)",
- "f2b_netban_ipv6": "차단할 IPv6 서브넷 크기 (8-128)",
- "f2b_parameters": "Fail2ban 변수",
- "f2b_retry_window": "최대 시도 횟수",
- "f2b_whitelist": "화이트리스트에 저장된 네트워크/호스트",
+ "netfilter_ban_time": "차단 시간 (s)",
+ "netfilter_blacklist": "블랙리스트에 기록된 네트워크/호스트",
+ "netfilter_list_info": "블랙리스트에 기록된 호스트 혹은 네트워크는 항상 화이트 리스트보다 먼저 고려됩니다. 리스트를 업데이트하면 적용되는데 시간이 조금 걸릴 수 있습니다. ",
+ "netfilter_max_attempts": "최대 시도 횟수",
+ "netfilter_netban_ipv4": "차단할 IPv4 서브넷 크기 (8-32)",
+ "netfilter_netban_ipv6": "차단할 IPv6 서브넷 크기 (8-128)",
+ "netfilter_parameters": "Netfilter 변수",
+ "netfilter_retry_window": "최대 시도 횟수",
+ "netfilter_whitelist": "화이트리스트에 저장된 네트워크/호스트",
"filter_table": "필터 테이블",
"forwarding_hosts": "포워딩 호스트",
"forwarding_hosts_add_hint": "IPv4/IPv6 주소, CIDR 방식의 네트워크, 호스트 이름 (IP 주소로 확인된 호스트 이름), 또는 도메인 이름 (SPF 레코드를 쿼리하거나 MX레코드가 없는 경우 IP 주소로 확인된 도메인 이름)을 지정할 수 있습니다.",
@@ -819,7 +819,7 @@
"domain_removed": "Domain %s has been removed",
"dovecot_restart_success": "Dovecot was restarted successfully",
"eas_reset": "ActiveSync devices for user %s were reset",
- "f2b_modified": "Changes to Fail2ban parameters have been saved",
+ "netfilter_modified": "Changes to Netfilter parameters have been saved",
"forwarding_host_added": "Forwarding host %s has been added",
"forwarding_host_removed": "Forwarding host %s has been removed",
"global_filter_written": "Filter was successfully written to file",
diff --git a/data/web/lang/lang.lv-lv.json b/data/web/lang/lang.lv-lv.json
index b03848a4..322db8dc 100644
--- a/data/web/lang/lang.lv-lv.json
+++ b/data/web/lang/lang.lv-lv.json
@@ -89,13 +89,13 @@
"domain_admins": "Domēna administratori",
"edit": "Labot",
"empty": "Nav rezultātu",
- "f2b_ban_time": "Aizlieguma laiks (s)",
- "f2b_max_attempts": "Maks. piegājieni",
- "f2b_netban_ipv4": "IPv4 apakštīkla izmērs, lai piemērotu aizliegumu uz (8-32)",
- "f2b_netban_ipv6": "IPv6 apakštīkla izmērs, lai piemērotu aizliegumu uz (8-128)",
- "f2b_parameters": "Fail2ban parametri",
- "f2b_retry_window": "Atkārtošanas logs (s) priekš maks. piegājiena",
- "f2b_whitelist": "Baltā saraksta tīkls/hosts",
+ "netfilter_ban_time": "Aizlieguma laiks (s)",
+ "netfilter_max_attempts": "Maks. piegājieni",
+ "netfilter_netban_ipv4": "IPv4 apakštīkla izmērs, lai piemērotu aizliegumu uz (8-32)",
+ "netfilter_netban_ipv6": "IPv6 apakštīkla izmērs, lai piemērotu aizliegumu uz (8-128)",
+ "netfilter_parameters": "Netfilter parametri",
+ "netfilter_retry_window": "Atkārtošanas logs (s) priekš maks. piegājiena",
+ "netfilter_whitelist": "Baltā saraksta tīkls/hosts",
"filter_table": "Filtru tabula",
"forwarding_hosts": "Hostu pārsūtīšana",
"forwarding_hosts_add_hint": "Jūs varat norādīt IPv4/IPv6 addreses, tīklu iekš CIDR apzīmējuma, hosta nosaukumu (kas tiks atrisinātas IP adresēs), vai domēna vārdos (kas tiks atrisināts IP adresēs vaicājot SPF ierakstus vai, ja tādu nav, MX ierakstus).",
@@ -426,7 +426,7 @@
"domain_modified": "Izmaiņas domēnam %s ir saglabātas",
"domain_removed": "Domēns %s ir noņemts",
"eas_reset": "ActiveSync ierīces priekš lietotāja %s tika atiestatītas",
- "f2b_modified": "Izmaiņas Fail2ban parameteriem ir saglabātas",
+ "netfilter_modified": "Izmaiņas Netfilter parameteriem ir saglabātas",
"forwarding_host_added": "Pāradresācijas hosts %s pievienotsd",
"forwarding_host_removed": "Pāradresācijas hosts %s noņemts",
"item_deleted": "Vērtība %s veiksmīgi dzēsta",
diff --git a/data/web/lang/lang.nl-nl.json b/data/web/lang/lang.nl-nl.json
index 547c7bb7..77d529b6 100644
--- a/data/web/lang/lang.nl-nl.json
+++ b/data/web/lang/lang.nl-nl.json
@@ -167,17 +167,17 @@
"edit": "Wijzig",
"empty": "Geen resultaten",
"excludes": "Exclusief",
- "f2b_ban_time": "Verbanningstijd (s)",
- "f2b_blacklist": "Netwerken/hosts op de blacklist",
- "f2b_filter": "Regex-filters",
- "f2b_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen. ",
- "f2b_max_attempts": "Maximaal aantal pogingen",
- "f2b_netban_ipv4": "Voer de IPv4-subnetgrootte in waar de verbanning van kracht moet zijn (8-32)",
- "f2b_netban_ipv6": "Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)",
- "f2b_parameters": "Fail2ban",
- "f2b_regex_info": "De volgende logs worden gebruikt: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Tijdsbestek voor maximale pogingen (s)",
- "f2b_whitelist": "Netwerken/hosts op de whitelist",
+ "netfilter_ban_time": "Verbanningstijd (s)",
+ "netfilter_blacklist": "Netwerken/hosts op de blacklist",
+ "netfilter_filter": "Regex-filters",
+ "netfilter_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen. ",
+ "netfilter_max_attempts": "Maximaal aantal pogingen",
+ "netfilter_netban_ipv4": "Voer de IPv4-subnetgrootte in waar de verbanning van kracht moet zijn (8-32)",
+ "netfilter_netban_ipv6": "Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)",
+ "netfilter_parameters": "Netfilter",
+ "netfilter_regex_info": "De volgende logs worden gebruikt: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Tijdsbestek voor maximale pogingen (s)",
+ "netfilter_whitelist": "Netwerken/hosts op de whitelist",
"filter_table": "Filtertabel",
"forwarding_hosts": "Forwarding hosts",
"forwarding_hosts_add_hint": "Het is mogelijk om IPv4- of IPv6-adressen, netwerken in CIDR-notatie, hostnames (worden omgezet naar IP-adressen) of domeinnamen (worden tevens omgezet naar IP-adressen of, bij gebrek daaraan, MX-records) op te geven.",
@@ -397,7 +397,7 @@
"redis_error": "Redis-error: %s",
"relayhost_invalid": "Invoer %s is ongeldig",
"release_send_failed": "Het volgende bericht kon niet worden vrijgegeven: %s",
- "reset_f2b_regex": "Regex-filters konden niet worden hersteld, probeer het opnieuw of herlaad de pagina over enkele seconden.",
+ "reset_netfilter_regex": "Regex-filters konden niet worden hersteld, probeer het opnieuw of herlaad de pagina over enkele seconden.",
"resource_invalid": "Naam van resource %s is ongeldig",
"rl_timeframe": "Ratelimit-tijdsbestek is ongeldig",
"rspamd_ui_pw_length": "Rspamd-wachtwoord dient minstens 6 tekens lang te zijn",
@@ -857,7 +857,7 @@
"domain_removed": "Domein %s is verwijderd",
"dovecot_restart_success": "Dovecot is succesvol herstart",
"eas_reset": "De ActiveSync-apparaatcache van gebruiker %s is hersteld",
- "f2b_modified": "Wijzigingen aan Fail2ban zijn opgeslagen",
+ "netfilter_modified": "Wijzigingen aan Netfilter zijn opgeslagen",
"forwarding_host_added": "Forwarding host %s is toegevoegd",
"forwarding_host_removed": "Forwarding host %s is verwijderd",
"global_filter_written": "Filter is opgeslagen",
diff --git a/data/web/lang/lang.pl-pl.json b/data/web/lang/lang.pl-pl.json
index b2862d8e..212021d6 100644
--- a/data/web/lang/lang.pl-pl.json
+++ b/data/web/lang/lang.pl-pl.json
@@ -66,11 +66,11 @@
"domain_admins": "Administratorzy domeny",
"edit": "Edytuj",
"empty": "Brak wyników",
- "f2b_ban_time": "Czas bana (s)",
- "f2b_max_attempts": "Max. ilość prób",
- "f2b_parameters": "Parametry Fail2ban",
- "f2b_retry_window": "Spróbuj ponownie (s) dla max. ilości prób",
- "f2b_whitelist": "Biała lista sieci/hosty",
+ "netfilter_ban_time": "Czas bana (s)",
+ "netfilter_max_attempts": "Max. ilość prób",
+ "netfilter_parameters": "Parametry Netfilter",
+ "netfilter_retry_window": "Spróbuj ponownie (s) dla max. ilości prób",
+ "netfilter_whitelist": "Biała lista sieci/hosty",
"filter_table": "Tabela filtru",
"forwarding_hosts": "Hosty przekazujące",
"forwarding_hosts_add_hint": "Możesz albo wyszczególnić adresy IPv4/IPv6, sieci w notacji CIDR, nazwy hostów (które zostaną rozłożone na adresy IP), albo nazwy domen (które zostaną rozłożone na adresy IP poprzez sprawdzanie rekordów SPF lub, w razie ich braku, rekordów MX).",
@@ -310,7 +310,7 @@
"domain_modified": "Zapisano zmiany w domenie %s",
"domain_removed": "Usunięto domenę %s",
"eas_reset": "Zresetowano urządzenia ActiveSync dla użytkownika %s",
- "f2b_modified": "Zmiany w Fail2ban zostały zapisane",
+ "netfilter_modified": "Zmiany w Netfilter zostały zapisane",
"forwarding_host_added": "Dodano hosta przekazującego %s",
"forwarding_host_removed": "Usunięto hosta przekazującego %s",
"item_deleted": "",
diff --git a/data/web/lang/lang.ro-ro.json b/data/web/lang/lang.ro-ro.json
index fe0d2064..61df8a39 100644
--- a/data/web/lang/lang.ro-ro.json
+++ b/data/web/lang/lang.ro-ro.json
@@ -175,17 +175,17 @@
"edit": "Editează",
"empty": "Nici un rezultat",
"excludes": "Exclude acești destinatari",
- "f2b_ban_time": "Timp interdicție (s)",
- "f2b_blacklist": "Rețele/gazde pe lista neagră",
- "f2b_filter": "Filtre regex",
- "f2b_list_info": "O gazdă sau o rețea pe lista neagră va depăși întotdeauna o entitate pe lista albă. Înregistrările din lista neagră sunt create la data încărcării containerului. Înregistrările din lista albă sunt citite de fiecare dată când se va aplica o interdicție.",
- "f2b_max_attempts": "Număr maxim încercări",
- "f2b_netban_ipv4": "Dimensiunea subrețelei IPv4 pentru a aplica interdicția (8-32)",
- "f2b_netban_ipv6": "Dimensiunea subrețelei IPv6 pentru a aplica interdicția (8-128)",
- "f2b_parameters": "Parametrii fail2ban",
- "f2b_regex_info": "Jurnalele luate în considerare: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Reîncercați fereastra (ele) pentru max. încercări",
- "f2b_whitelist": "Rețele/gazde pe lista albă",
+ "netfilter_ban_time": "Timp interdicție (s)",
+ "netfilter_blacklist": "Rețele/gazde pe lista neagră",
+ "netfilter_filter": "Filtre regex",
+ "netfilter_list_info": "O gazdă sau o rețea pe lista neagră va depăși întotdeauna o entitate pe lista albă. Înregistrările din lista neagră sunt create la data încărcării containerului. Înregistrările din lista albă sunt citite de fiecare dată când se va aplica o interdicție.",
+ "netfilter_max_attempts": "Număr maxim încercări",
+ "netfilter_netban_ipv4": "Dimensiunea subrețelei IPv4 pentru a aplica interdicția (8-32)",
+ "netfilter_netban_ipv6": "Dimensiunea subrețelei IPv6 pentru a aplica interdicția (8-128)",
+ "netfilter_parameters": "Parametrii Netfilter",
+ "netfilter_regex_info": "Jurnalele luate în considerare: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Reîncercați fereastra (ele) pentru max. încercări",
+ "netfilter_whitelist": "Rețele/gazde pe lista albă",
"filter_table": "Tabel filtre",
"forwarding_hosts": "Gazde de redirecționare",
"forwarding_hosts_add_hint": "Poți specifica fie adrese IPv4 / IPv6, rețele în notație CIDR, nume gazdă (care vor fi rezolvate la adrese IP), fie nume de domenii (care vor fi rezolvate la adrese IP prin interogarea înregistrărilor SPF sau, în absența acestora, a înregistrărilor MX).",
@@ -423,7 +423,7 @@
"redis_error": "Eroare Redis: %s",
"relayhost_invalid": "Intrarea hărții %s este invalidă",
"release_send_failed": "Mesajul nu a putut fi eliberat: %s",
- "reset_f2b_regex": "Filtrul regex nu a putut fi resetat la timp, încercați din nou sau așteptați câteva secunde și reîncărcați pagina.",
+ "reset_netfilter_regex": "Filtrul regex nu a putut fi resetat la timp, încercați din nou sau așteptați câteva secunde și reîncărcați pagina.",
"resource_invalid": "Numele de resurse %s este invalid",
"rl_timeframe": "Rata limită pentru intervalul de timp este incorectă",
"rspamd_ui_pw_length": "Parola Rspamd UI ar trebui să aibă cel puțin 6 caractere",
@@ -944,7 +944,7 @@
"domain_removed": "Domeniul %s a fost adăugat",
"dovecot_restart_success": "Dovecot a fost restartat cu succes",
"eas_reset": "Dispozitivele ActiveSync pentru utilizatorul %s au fost resetate",
- "f2b_modified": "Modificările parametrilor Fail2ban au fost salvate",
+ "netfilter_modified": "Modificările parametrilor Netfilter au fost salvate",
"forwarding_host_added": "Gazda de redirecționare %s a fost adăugată",
"forwarding_host_removed": "Gazda de redirecționare %s a fost eliminată",
"global_filter_written": "Filtru a fost scris cu succes în fișier",
diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json
index 60aba927..f684a4d2 100644
--- a/data/web/lang/lang.ru-ru.json
+++ b/data/web/lang/lang.ru-ru.json
@@ -176,17 +176,17 @@
"edit": "Изменить",
"empty": "Пусто",
"excludes": "Исключает этих получателей",
- "f2b_ban_time": "Время бана (в секундах)",
- "f2b_blacklist": "Черный список подсетей/хостов",
- "f2b_filter": "Правила фильтрации с помощью регулярных выражений",
- "f2b_list_info": "Хосты или подсети, занесенные в черный список, всегда будут перевешивать объекты из белого списка. Обновление списка займет несколько секунд. ",
- "f2b_max_attempts": "Максимальное количество попыток",
- "f2b_netban_ipv4": "Размер подсети IPv4 для применения бана (8-32)",
- "f2b_netban_ipv6": "Размер подсети IPv6 для применения бана (8-128)",
- "f2b_parameters": "Настройки Fail2ban",
- "f2b_regex_info": "Журналы которые принимаются во внимание: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Промежуток времени для следующего бана (в секундах)",
- "f2b_whitelist": "Белый список подсетей/хостов",
+ "netfilter_ban_time": "Время бана (в секундах)",
+ "netfilter_blacklist": "Черный список подсетей/хостов",
+ "netfilter_filter": "Правила фильтрации с помощью регулярных выражений",
+ "netfilter_list_info": "Хосты или подсети, занесенные в черный список, всегда будут перевешивать объекты из белого списка. Обновление списка займет несколько секунд. ",
+ "netfilter_max_attempts": "Максимальное количество попыток",
+ "netfilter_netban_ipv4": "Размер подсети IPv4 для применения бана (8-32)",
+ "netfilter_netban_ipv6": "Размер подсети IPv6 для применения бана (8-128)",
+ "netfilter_parameters": "Настройки Netfilter",
+ "netfilter_regex_info": "Журналы которые принимаются во внимание: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Промежуток времени для следующего бана (в секундах)",
+ "netfilter_whitelist": "Белый список подсетей/хостов",
"filter_table": "Поиск",
"forwarding_hosts": "Переадресация хостов",
"forwarding_hosts_add_hint": "Можно указывать: IPv4/IPv6 подсети в нотации CIDR, имена хостов (которые будут разрешаться в IP-адреса) или доменные имена (которые будут решаться с IP-адресами путем запроса SPF записей или, в случае их отсутствия - запросом MX записей).",
@@ -425,7 +425,7 @@
"redis_error": "Ошибка в Redis: %s",
"relayhost_invalid": "Правило %s не валидное",
"release_send_failed": "Сообщение не может быть восстановлено: %s",
- "reset_f2b_regex": "Сброс фильтров не был выполнен за отведённый промежуток времени, пожалуйста, повторите попытку или подождите еще несколько секунд и перезагрузите веб страницу.",
+ "reset_netfilter_regex": "Сброс фильтров не был выполнен за отведённый промежуток времени, пожалуйста, повторите попытку или подождите еще несколько секунд и перезагрузите веб страницу.",
"resource_invalid": "Недопустимое имя ресурса",
"rl_timeframe": "Не верный временной интервал для лимита отправки",
"rspamd_ui_pw_length": "Длина пароля должна составлять не менее 6 символов для Rspamd UI",
@@ -942,7 +942,7 @@
"domain_removed": "Домен %s удалён",
"dovecot_restart_success": "Dovecot перезапущен успешно",
"eas_reset": "Кеш ActiveSync для пользователя %s был сброшен",
- "f2b_modified": "Изменения параметров Fail2ban сохранены",
+ "netfilter_modified": "Изменения параметров Netfilter сохранены",
"forwarding_host_added": "Перенаправление узла %s добавлено",
"forwarding_host_removed": "Перенаправление узла %s удалено",
"global_filter_written": "Фильтр успешно записан в файл",
diff --git a/data/web/lang/lang.sk-sk.json b/data/web/lang/lang.sk-sk.json
index b6933b28..455629b5 100644
--- a/data/web/lang/lang.sk-sk.json
+++ b/data/web/lang/lang.sk-sk.json
@@ -175,17 +175,17 @@
"edit": "Upraviť",
"empty": "Žiadne výsledky",
"excludes": "Vylúčiť týchto príjemcov",
- "f2b_ban_time": "Dĺžka blokovania (s)",
- "f2b_blacklist": "Blacklist sietí/hostiteľov",
- "f2b_filter": "Regex filtre",
- "f2b_list_info": "Blacklist hostiteľov alebo sietí vždy preváži nad whitelistom. Aktualizácia zoznamov potrvá niekoľko sekúnd. ",
- "f2b_max_attempts": "Max. počet pokusov",
- "f2b_netban_ipv4": "Veľkosť IPv4 subnetu na ktorý sa aplikuje zákaz (8-32)",
- "f2b_netban_ipv6": "Veľkosť IPv6 subnetu na ktorý sa aplikuje zákaz (8-128)",
- "f2b_parameters": "Parametre automatického firewallu",
- "f2b_regex_info": "Záznamy ktoré sa berú do úvahy: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Čas v ktorom je treba uplatniť max. počet pokusov (s)",
- "f2b_whitelist": "Whitelist sietí/hostiteľov",
+ "netfilter_ban_time": "Dĺžka blokovania (s)",
+ "netfilter_blacklist": "Blacklist sietí/hostiteľov",
+ "netfilter_filter": "Regex filtre",
+ "netfilter_list_info": "Blacklist hostiteľov alebo sietí vždy preváži nad whitelistom. Aktualizácia zoznamov potrvá niekoľko sekúnd. ",
+ "netfilter_max_attempts": "Max. počet pokusov",
+ "netfilter_netban_ipv4": "Veľkosť IPv4 subnetu na ktorý sa aplikuje zákaz (8-32)",
+ "netfilter_netban_ipv6": "Veľkosť IPv6 subnetu na ktorý sa aplikuje zákaz (8-128)",
+ "netfilter_parameters": "Parametre automatického firewallu",
+ "netfilter_regex_info": "Záznamy ktoré sa berú do úvahy: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Čas v ktorom je treba uplatniť max. počet pokusov (s)",
+ "netfilter_whitelist": "Whitelist sietí/hostiteľov",
"filter_table": "Tabuľka filtrov",
"forwarding_hosts": "Preposielacie servery",
"forwarding_hosts_add_hint": "Môžete buď špecifikovať IPv4/IPv6 adresy, siete v CIDR notácii, názvy serverov (ktoré budú preložené na IP adresy), alebo doménové mená (ktoré budú ako IP získané z SPF záznamov, alebo v prípade ich neprítomnosti, pomocou MX záznamov).",
@@ -424,7 +424,7 @@
"redis_error": "Redis chyba: %s",
"relayhost_invalid": "Položka %s je neplatná",
"release_send_failed": "Správa nemohla byť uvoľnená: %s",
- "reset_f2b_regex": "Regex filter sa nepodarilo resetovať, skúste to znovu alebo počkajte pár sekúnd a obnovte stránku.",
+ "reset_netfilter_regex": "Regex filter sa nepodarilo resetovať, skúste to znovu alebo počkajte pár sekúnd a obnovte stránku.",
"resource_invalid": "Zdrojové meno %s je neplatné",
"rl_timeframe": "Obmedzenie časového rámca je nesprávny",
"rspamd_ui_pw_length": "Heslo pre Rspamd rozhranie by malo mať minimálne 6 znakov",
@@ -944,7 +944,7 @@
"domain_removed": "Doména %s odstránená",
"dovecot_restart_success": "Dovecot sa úspešne reštartoval",
"eas_reset": "ActiveSync zariadenia pre používateľa %s resetovaný",
- "f2b_modified": "Zmeny v parametroch automatického firewallu uložené",
+ "netfilter_modified": "Zmeny v parametroch automatického firewallu uložené",
"forwarding_host_added": "Doručovací hostiteľ %s pridaný",
"forwarding_host_removed": "Doručovací hostiteľ %s vymazaný",
"global_filter_written": "Filter bol úspešne zapísaný do súboru",
diff --git a/data/web/lang/lang.sv-se.json b/data/web/lang/lang.sv-se.json
index 4cc84619..409c6c0b 100644
--- a/data/web/lang/lang.sv-se.json
+++ b/data/web/lang/lang.sv-se.json
@@ -173,17 +173,17 @@
"edit": "Ändra",
"empty": "Inga resultat",
"excludes": "Exkludera dessa mottagare",
- "f2b_ban_time": "Banlysning i sekunder",
- "f2b_blacklist": "Svartlistade nätverk/värdar",
- "f2b_filter": "Regex-filter",
- "f2b_list_info": "En värd eller ett nätverk som finns på en svartlista kommer alltid stå över en vitlista. Uppdateringar av listan kan ta några sekunder att genomföra. ",
- "f2b_max_attempts": "Maximalt antal försök",
- "f2b_netban_ipv4": "Storleken på ett IPv4 subnät, att applicera banlysning på (8-32)",
- "f2b_netban_ipv6": "Storleken på ett IPv6 subnät, att applicera banlysning på (8-128)",
- "f2b_parameters": "Fail2ban parametrar",
- "f2b_regex_info": "Loggarna avser: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Tidsfönster för antal försök",
- "f2b_whitelist": "Vitlistade nätverk/värdar",
+ "netfilter_ban_time": "Banlysning i sekunder",
+ "netfilter_blacklist": "Svartlistade nätverk/värdar",
+ "netfilter_filter": "Regex-filter",
+ "netfilter_list_info": "En värd eller ett nätverk som finns på en svartlista kommer alltid stå över en vitlista. Uppdateringar av listan kan ta några sekunder att genomföra. ",
+ "netfilter_max_attempts": "Maximalt antal försök",
+ "netfilter_netban_ipv4": "Storleken på ett IPv4 subnät, att applicera banlysning på (8-32)",
+ "netfilter_netban_ipv6": "Storleken på ett IPv6 subnät, att applicera banlysning på (8-128)",
+ "netfilter_parameters": "Netfilter parametrar",
+ "netfilter_regex_info": "Loggarna avser: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Tidsfönster för antal försök",
+ "netfilter_whitelist": "Vitlistade nätverk/värdar",
"filter_table": "Filtrera tebellen",
"forwarding_hosts": "Värdar för inkommande e-post servrar",
"forwarding_hosts_add_hint": "Du kan antingen specifiera IPv4-/IPv6-adresser, nätverk med CIDR betäckning, värdnamn (hämtar IP-adressen), eller domännamn (hämtar IP-adressen från SPF-uppslaget, eller från MX-uppslag).",
@@ -414,7 +414,7 @@
"redis_error": "Redis fel: %s",
"relayhost_invalid": "Posten %s är ogiltig",
"release_send_failed": "Meddelandet kunde inte skickas: %s",
- "reset_f2b_regex": "Regex-filtret kunde inte återställas inom en rimlig tid, försök igen eller ladda om sidan.",
+ "reset_netfilter_regex": "Regex-filtret kunde inte återställas inom en rimlig tid, försök igen eller ladda om sidan.",
"resource_invalid": "Resursnamnet %s är ogiltigt",
"rl_timeframe": "Intervallet för hastighetsbegränsningen är felaktig",
"rspamd_ui_pw_length": "Lösenordet för Rspamd UI måste vara minst 6 tecken långt",
@@ -882,7 +882,7 @@
"domain_removed": "Domänen %s har tagits bort",
"dovecot_restart_success": "Dovecot kunde startas om",
"eas_reset": "AktivSynk enheter för användaren %s har återställts",
- "f2b_modified": "Ändringarna på Fail2ban parametrarna har sparats",
+ "netfilter_modified": "Ändringarna på Netfilter parametrarna har sparats",
"forwarding_host_added": "Vidarbefodringsvärden %s har lagts till",
"forwarding_host_removed": "Vidarbefodringsvärden %s har tagits bort",
"global_filter_written": "Data har skrivits till filterfilen",
diff --git a/data/web/lang/lang.tr-tr.json b/data/web/lang/lang.tr-tr.json
index 20091db1..e2310ee8 100644
--- a/data/web/lang/lang.tr-tr.json
+++ b/data/web/lang/lang.tr-tr.json
@@ -79,8 +79,8 @@
"domain_s": "Alan ad(ları)",
"duplicate": "Çift",
"duplicate_dkim": "Çift DKIM kayıtları",
- "f2b_ban_time": "Yasaklama süresi (saniye)",
- "f2b_max_attempts": "Maksimum giriş denemesi",
- "f2b_retry_window": "Maksimum girişim için deneme pencere(leri)"
+ "netfilter_ban_time": "Yasaklama süresi (saniye)",
+ "netfilter_max_attempts": "Maksimum giriş denemesi",
+ "netfilter_retry_window": "Maksimum girişim için deneme pencere(leri)"
}
}
diff --git a/data/web/lang/lang.uk-ua.json b/data/web/lang/lang.uk-ua.json
index e3acb8b5..0b9d3360 100644
--- a/data/web/lang/lang.uk-ua.json
+++ b/data/web/lang/lang.uk-ua.json
@@ -162,15 +162,15 @@
"duplicate": "Копіювати",
"edit": "Змінити",
"empty": "Пусто",
- "f2b_ban_time": "Час бану (у секундах)",
- "f2b_blacklist": "Чорний список підмереж/хостів",
- "f2b_max_attempts": "Максимальна кількість спроб",
- "f2b_netban_ipv4": "Розмір підмережі IPv4 для блокування (8-32)",
- "f2b_netban_ipv6": "Розмір підмережі IPv6 для блокування (8-128)",
- "f2b_parameters": "Налаштування Fail2ban",
- "f2b_regex_info": "Журнали, які приймаються до уваги: SOGo, Postfix, Dovecot, PHP-FPM.",
- "f2b_retry_window": "Проміжок часу для наступного блокування (у секундах)",
- "f2b_whitelist": "Білий список підмереж/хостів",
+ "netfilter_ban_time": "Час бану (у секундах)",
+ "netfilter_blacklist": "Чорний список підмереж/хостів",
+ "netfilter_max_attempts": "Максимальна кількість спроб",
+ "netfilter_netban_ipv4": "Розмір підмережі IPv4 для блокування (8-32)",
+ "netfilter_netban_ipv6": "Розмір підмережі IPv6 для блокування (8-128)",
+ "netfilter_parameters": "Налаштування Netfilter",
+ "netfilter_regex_info": "Журнали, які приймаються до уваги: SOGo, Postfix, Dovecot, PHP-FPM.",
+ "netfilter_retry_window": "Проміжок часу для наступного блокування (у секундах)",
+ "netfilter_whitelist": "Білий список підмереж/хостів",
"filter_table": "Пошук",
"forwarding_hosts": "Переадресація хостів",
"from": "Від",
@@ -303,8 +303,8 @@
"dkim_to_title": "Цільовий домен(и) (DKIM буде перезаписаний)",
"duplicate_dkim": "Копіювання DKIM запису",
"excludes": "Виключає цих отримувачів",
- "f2b_filter": "Правила фільтрування за допомогою регулярних виразів",
- "f2b_list_info": "Хости або підмережі, занесені до чорного списку, завжди переважатимуть об'єкти з білого списку. Оновлення списку займе декілька секунд. ",
+ "netfilter_filter": "Правила фільтрування за допомогою регулярних виразів",
+ "netfilter_list_info": "Хости або підмережі, занесені до чорного списку, завжди переважатимуть об'єкти з білого списку. Оновлення списку займе декілька секунд. ",
"forwarding_hosts_add_hint": "Можна вказувати: IPv4/IPv6 підмережі в нотації CIDR, імена хостів (які будуть дозволятися в IP-адреси) або доменні імена (які будуть вирішуватися з IP-адресами шляхом запиту SPF записів або, у разі їх відсутності, запитом MX записів).",
"forwarding_hosts_hint": "Вхідні повідомлення приймаються від будь-яких хостів, перерахованих тут. Ці хости не проходять перевірку DNSBL і greylisting. Спам, отриманий від них, ніколи не відхиляється, але за бажання можна включити спам-фільтр і листи з поганим рейтингом потраплятимуть до спаму. Найбільш поширене використання – вказати поштові сервери, на яких ви встановили правило, яке перенаправляє вхідні електронні листи на ваш поштовий сервер mailcow.",
"guid_and_license": "Ліцензія та GUID",
@@ -446,7 +446,7 @@
"private_key_error": "Помилка приватного ключа: %s",
"pushover_token": "Токен Pushover вказано у неправильному форматі",
"relayhost_invalid": "Правило %s невірне",
- "reset_f2b_regex": "Скидання фільтрів не було виконано за відведений проміжок часу, будь ласка, повторіть спробу або зачекайте ще кілька секунд і перезавантажте веб-сторінку.",
+ "reset_netfilter_regex": "Скидання фільтрів не було виконано за відведений проміжок часу, будь ласка, повторіть спробу або зачекайте ще кілька секунд і перезавантажте веб-сторінку.",
"sender_acl_invalid": "Неприпустиме значення ACL для %s",
"spam_learn_error": "Помилка при навчанні спам фільтра: %s",
"targetd_relay_domain": "Цільовий домен %s є доменом ретрансляції",
@@ -977,7 +977,7 @@
"deleted_syncjobs": "Завдання синхронізації видалені: %s",
"domain_admin_added": "Адміністратора домену %s додано",
"domain_modified": "Зберегти зміни домену %s",
- "f2b_modified": "Зміни параметрів Fail2ban збережено",
+ "netfilter_modified": "Зміни параметрів Netfilter збережено",
"global_filter_written": "Фільтр успішно записано у файл",
"items_deleted": "Елемент %s успішно видалено",
"license_modified": "Зміни у ліцензії збережені",
diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json
index 5d05ef62..842689e1 100644
--- a/data/web/lang/lang.zh-cn.json
+++ b/data/web/lang/lang.zh-cn.json
@@ -176,17 +176,17 @@
"edit": "编辑",
"empty": "结果为空",
"excludes": "除了",
- "f2b_ban_time": "封禁时间 (秒)",
- "f2b_blacklist": "网络/主机黑名单",
- "f2b_filter": "正则表达式过滤器",
- "f2b_list_info": "黑名单的优先级总是高于白名单。 列表更新将会在几秒之后完成。 ",
- "f2b_max_attempts": "最多尝试次数",
- "f2b_netban_ipv4": "应用封禁的 IPv4 子网大小 (8-32)",
- "f2b_netban_ipv6": "应用封禁的 IPv6 子网大小 (8-128)",
- "f2b_parameters": "Fail2ban 参数",
- "f2b_regex_info": "将会过滤这些应用的日志: SOGo,Postfix,Dovecot 和 PHP-FPM。",
- "f2b_retry_window": "最多尝试次数重试窗口 (秒)",
- "f2b_whitelist": "网络/主机白名单",
+ "netfilter_ban_time": "封禁时间 (秒)",
+ "netfilter_blacklist": "网络/主机黑名单",
+ "netfilter_filter": "正则表达式过滤器",
+ "netfilter_list_info": "黑名单的优先级总是高于白名单。 列表更新将会在几秒之后完成。 ",
+ "netfilter_max_attempts": "最多尝试次数",
+ "netfilter_netban_ipv4": "应用封禁的 IPv4 子网大小 (8-32)",
+ "netfilter_netban_ipv6": "应用封禁的 IPv6 子网大小 (8-128)",
+ "netfilter_parameters": "Netfilter 参数",
+ "netfilter_regex_info": "将会过滤这些应用的日志: SOGo,Postfix,Dovecot 和 PHP-FPM。",
+ "netfilter_retry_window": "最多尝试次数重试窗口 (秒)",
+ "netfilter_whitelist": "网络/主机白名单",
"filter_table": "筛选表格",
"forwarding_hosts": "转发主机",
"forwarding_hosts_add_hint": "你可以指定 IPv4/IPv6 地址、CIDR 表示的网络、主机名 (解析为 IP 地址),或者邮箱域名 (查询 SPF 记录或 MX 记录并解析为 IP 地址)。",
@@ -426,7 +426,7 @@
"redis_error": "Redis 错误: %s",
"relayhost_invalid": "中继主机条目 %s 已存在",
"release_send_failed": "消息不能被释放: %s",
- "reset_f2b_regex": "暂时不能重置正则表达式过滤器,请重试或在几秒后重载网页。",
+ "reset_netfilter_regex": "暂时不能重置正则表达式过滤器,请重试或在几秒后重载网页。",
"resource_invalid": "资源名称 %s 无效",
"rl_timeframe": "频率限制时间设置错误",
"rspamd_ui_pw_length": "Rspamd UI 密码至少为为6个字符",
@@ -947,7 +947,7 @@
"domain_removed": "已删除域名 %s",
"dovecot_restart_success": "Dovecot 重新启动成功",
"eas_reset": "已重置用户 %s 的 ActiveSync 设备",
- "f2b_modified": "已保存 Fail2ban 参数的更改",
+ "netfilter_modified": "已保存 Netfilter 参数的更改",
"forwarding_host_added": "已添加转发主机 %s",
"forwarding_host_removed": "已删除转发主机 %s",
"global_filter_written": "成功将过滤器写入到文件",
diff --git a/data/web/lang/lang.zh-tw.json b/data/web/lang/lang.zh-tw.json
index f9c81c66..2ea107fb 100644
--- a/data/web/lang/lang.zh-tw.json
+++ b/data/web/lang/lang.zh-tw.json
@@ -176,17 +176,17 @@
"edit": "編輯",
"empty": "沒有結果",
"excludes": "排除這些收件者",
- "f2b_ban_time": "封鎖時間 (秒)",
- "f2b_blacklist": "網路/主機黑名單",
- "f2b_filter": "正規表示式過濾器",
- "f2b_list_info": "黑名單優先於白名單。 清單更新需要幾秒才能套用。 ",
- "f2b_max_attempts": "嘗試次數上限",
- "f2b_netban_ipv4": "封鎖的 IPv4 子網路大小 (8-32)",
- "f2b_netban_ipv6": "封鎖的 IPv6 子網路大小 (8-128)",
- "f2b_parameters": "Fail2ban 參數",
- "f2b_regex_info": "納入過濾參考的應用程式紀錄: SOGo、Postfix、Dovecot、PHP-FPM。",
- "f2b_retry_window": "重試次數的時間區間大小 (秒)",
- "f2b_whitelist": "網路/主機白名單",
+ "netfilter_ban_time": "封鎖時間 (秒)",
+ "netfilter_blacklist": "網路/主機黑名單",
+ "netfilter_filter": "正規表示式過濾器",
+ "netfilter_list_info": "黑名單優先於白名單。 清單更新需要幾秒才能套用。 ",
+ "netfilter_max_attempts": "嘗試次數上限",
+ "netfilter_netban_ipv4": "封鎖的 IPv4 子網路大小 (8-32)",
+ "netfilter_netban_ipv6": "封鎖的 IPv6 子網路大小 (8-128)",
+ "netfilter_parameters": "Netfilter 參數",
+ "netfilter_regex_info": "納入過濾參考的應用程式紀錄: SOGo、Postfix、Dovecot、PHP-FPM。",
+ "netfilter_retry_window": "重試次數的時間區間大小 (秒)",
+ "netfilter_whitelist": "網路/主機白名單",
"filter_table": "篩選表格",
"forwarding_hosts": "轉發主機",
"forwarding_hosts_add_hint": "你可以指定 IPv4/IPv6 地址、CIDR 表示的網路、主機名稱 (會被自動解析為 IP 地址),或者信箱域名 (會自動查詢 SPF 紀錄或 MX 紀錄並解析為 IP 地址)。",
@@ -425,7 +425,7 @@
"redis_error": "Redis 錯誤: %s",
"relayhost_invalid": "無效的中繼主機規則 %s",
"release_send_failed": "郵件無法被釋放: %s",
- "reset_f2b_regex": "暫時無法重設正規表示式過濾器,請重試或在稍後重新載入網頁。",
+ "reset_netfilter_regex": "暫時無法重設正規表示式過濾器,請重試或在稍後重新載入網頁。",
"resource_invalid": "無效的資源名稱 %s",
"rl_timeframe": "速率限制間隔時間不正確",
"rspamd_ui_pw_length": "Rspamd UI 密碼至少要 6 個字元",
@@ -941,7 +941,7 @@
"domain_removed": "域名 %s 已刪除",
"dovecot_restart_success": "Dovecot 成功重新啟動",
"eas_reset": "使用者 %s 的 ActiveSync 裝置已重設",
- "f2b_modified": "Fail2ban 參數更改已儲存",
+ "netfilter_modified": "Netfilter 參數更改已儲存",
"forwarding_host_added": "轉發主機 %s 已新增",
"forwarding_host_removed": "轉發主機 %s 已刪除",
"global_filter_written": "過濾器已成功寫入到檔案",
diff --git a/data/web/templates/admin.twig b/data/web/templates/admin.twig
index 863f87e9..77fb0ec2 100644
--- a/data/web/templates/admin.twig
+++ b/data/web/templates/admin.twig
@@ -18,7 +18,7 @@