diff --git a/.gitignore b/.gitignore index 867de80c..e535c710 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,3 @@ data/conf/nginx/*.conf data/conf/nginx/*.custom data/conf/nginx/*.bak data/conf/dovecot/extra.conf -data/conf/rspamd/override.d/worker-controller-password.inc diff --git a/data/Dockerfiles/clamd/bootstrap.sh b/data/Dockerfiles/clamd/bootstrap.sh index ffe582c9..c4c9e04c 100755 --- a/data/Dockerfiles/clamd/bootstrap.sh +++ b/data/Dockerfiles/clamd/bootstrap.sh @@ -7,6 +7,7 @@ if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then fi # Create log pipes +mkdir /var/log/clamav touch /var/log/clamav/clamd.log /var/log/clamav/freshclam.log mkfifo -m 600 /tmp/logpipe_clamd mkfifo -m 600 /tmp/logpipe_freshclam diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index a5b3301d..3f205b8b 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -2,7 +2,7 @@ FROM python:2-alpine LABEL maintainer "Andre Peters " RUN apk add -U --no-cache iptables ip6tables -RUN pip install docker flask flask-restful +RUN pip install docker==3.0.1 flask flask-restful COPY server.py / CMD ["python2", "-u", "/server.py"] diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py index 7410c0ff..57ed3570 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -22,7 +22,7 @@ class containers_get(Resource): containers.update({container.attrs['Id']: container.attrs}) return containers except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) class container_get(Resource): def get(self, container_id): @@ -31,7 +31,7 @@ class container_get(Resource): for container in docker_client.containers.list(all=True, filters={"id": container_id}): return container.attrs except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) else: return jsonify(type='danger', msg='no or invalid id defined') @@ -44,7 +44,7 @@ class container_post(Resource): container.stop() return jsonify(type='success', msg='command completed successfully') except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) elif post_action == 'start': try: @@ -52,7 +52,7 @@ class container_post(Resource): container.start() return jsonify(type='success', msg='command completed successfully') except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) elif post_action == 'restart': try: @@ -60,36 +60,53 @@ class container_post(Resource): container.restart() return jsonify(type='success', msg='command completed successfully') except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) elif post_action == 'exec': if not request.json or not 'cmd' in request.json: return jsonify(type='danger', msg='cmd is missing') - if request.json['cmd'] == 'sieve_list' and request.json['username']: + if request.json['cmd'] == 'df' and request.json['dir']: try: for container in docker_client.containers.list(filters={"id": container_id}): - return container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') + # Should be changed to be able to validate a path + directory = re.sub('[^0-9a-zA-Z/]+', '', request.json['dir']) + df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H " + directory + " | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') + if df_return.exit_code == 0: + return df_return.output.rstrip() + else: + return "0,0,0,0,0,0" except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) + elif request.json['cmd'] == 'sieve_list' and request.json['username']: + try: + for container in docker_client.containers.list(filters={"id": container_id}): + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') + return sieve_return.output + except Exception as e: + return jsonify(type='danger', msg=str(e)) elif request.json['cmd'] == 'sieve_print' and request.json['script_name'] and request.json['username']: try: for container in docker_client.containers.list(filters={"id": container_id}): return container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"], user='vmail') except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) elif request.json['cmd'] == 'worker_password' and request.json['raw']: try: for container in docker_client.containers.list(filters={"id": container_id}): hash = container.exec_run(["/bin/bash", "-c", "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null"], user='_rspamd') - f = open("/access.inc", "w") - f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash.rstrip()) + '";\n') - f.close() - container.restart() - return jsonify(type='success', msg='command completed successfully') + if hash.exit_code == 0: + hash = str(hash.output) + f = open("/access.inc", "w") + f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash.rstrip()) + '";\n') + f.close() + container.restart() + return jsonify(type='success', msg='command completed successfully') + else: + return jsonify(type='danger', msg='command did not complete, exit code was ' + int(hash.exit_code)) except Exception as e: - return jsonify(type='danger', msg=e) + return jsonify(type='danger', msg=str(e)) else: return jsonify(type='danger', msg='Unknown command') diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 9f8f5313..b1f78870 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -82,7 +82,7 @@ cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf driver = mysql connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" default_pass_scheme = SSHA256 -password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') +password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_EXTRACT(attributes, "$.force_pw_update") NOT LIKE '%1%' user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' iterate_query = SELECT username FROM mailbox WHERE active='1'; EOF diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham index 7c3ab03f..cca3d8a0 100755 --- a/data/Dockerfiles/dovecot/rspamd-pipe-ham +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -1,4 +1,4 @@ #!/bin/bash -/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnham < /dev/stdin +/usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnham < /dev/stdin # Always return 0 to satisfy Dovecot... exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam index 67cccb2c..3adbc960 100755 --- a/data/Dockerfiles/dovecot/rspamd-pipe-spam +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -1,4 +1,4 @@ #!/bin/bash -/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnspam < /dev/stdin +/usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnspam < /dev/stdin # Always return 0 to satisfy Dovecot... exit 0 diff --git a/data/Dockerfiles/fail2ban/Dockerfile b/data/Dockerfiles/fail2ban/Dockerfile deleted file mode 100644 index 26fe9414..00000000 --- a/data/Dockerfiles/fail2ban/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM python:2-alpine -LABEL maintainer "Andre Peters " - -RUN apk add -U --no-cache iptables ip6tables -RUN pip install redis ipaddress - -COPY logwatch.py / -CMD ["python2", "-u", "/logwatch.py"] diff --git a/data/Dockerfiles/fail2ban/logwatch.py b/data/Dockerfiles/fail2ban/logwatch.py deleted file mode 100644 index e6f6d099..00000000 --- a/data/Dockerfiles/fail2ban/logwatch.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python2 - -import re -import os -import time -import atexit -import signal -import ipaddress -import subprocess -from threading import Thread -import redis -import time -import json - -yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$') -if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)): - print 'SKIP_FAIL2BAN=y, Skipping Fail2ban container...' - time.sleep(31536000) - raise SystemExit - -r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0) -pubsub = r.pubsub() - -RULES = {} -RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' -RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' -RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' -RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' -RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' -RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' - -r.setnx('F2B_BAN_TIME', '1800') -r.setnx('F2B_MAX_ATTEMPTS', '10') -r.setnx('F2B_RETRY_WINDOW', '600') -r.setnx('F2B_NETBAN_IPV6', '64') -r.setnx('F2B_NETBAN_IPV4', '24') - -bans = {} -log = {} -quit_now = False - -def ban(address): - BAN_TIME = int(r.get('F2B_BAN_TIME')) - MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS')) - RETRY_WINDOW = int(r.get('F2B_RETRY_WINDOW')) - WHITELIST = r.hgetall('F2B_WHITELIST') - NETBAN_IPV6 = '/' + str(r.get('F2B_NETBAN_IPV6')) - NETBAN_IPV4 = '/' + str(r.get('F2B_NETBAN_IPV4')) - - ip = ipaddress.ip_address(address.decode('ascii')) - 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.decode('ascii')) - if WHITELIST: - for wl_key in WHITELIST: - wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) - if wl_net.overlaps(self_network): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net) - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - print '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)).decode('ascii'), 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: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Banning %s' % net - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - print 'Banning %s for %d minutes' % (net, BAN_TIME / 60) - if type(ip) is ipaddress.IPv4Address: - subprocess.call(['iptables', '-I', 'INPUT', '-s', net, '-j', 'REJECT']) - subprocess.call(['iptables', '-I', 'FORWARD', '-s', net, '-j', 'REJECT']) - else: - subprocess.call(['ip6tables', '-I', 'INPUT', '-s', net, '-j', 'REJECT']) - subprocess.call(['ip6tables', '-I', 'FORWARD', '-s', net, '-j', 'REJECT']) - r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME) - else: - log['time'] = int(round(time.time())) - log['priority'] = 'warn' - log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) - -def unban(net): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - if not net in bans: - log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - print '%s is not banned, skipping unban and deleting from queue (if any)' % net - r.hdel('F2B_QUEUE_UNBAN', '%s' % net) - return - log['message'] = 'Unbanning %s' % net - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - print 'Unbanning %s' % net - if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: - subprocess.call(['iptables', '-D', 'INPUT', '-s', net, '-j', 'REJECT']) - subprocess.call(['iptables', '-D', 'FORWARD', '-s', net, '-j', 'REJECT']) - else: - subprocess.call(['ip6tables', '-D', 'INPUT', '-s', net, '-j', 'REJECT']) - subprocess.call(['ip6tables', '-D', 'FORWARD', '-s', net, '-j', 'REJECT']) - r.hdel('F2B_ACTIVE_BANS', '%s' % net) - r.hdel('F2B_QUEUE_UNBAN', '%s' % net) - del bans[net] - -def quit(signum, frame): - global quit_now - quit_now = True - -def clear(): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Clearing all bans' - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - print 'Clearing all bans' - for net in bans.copy(): - unban(net) - pubsub.unsubscribe() - -def watch(): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Watching Redis channel F2B_CHANNEL' - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - pubsub.subscribe('F2B_CHANNEL') - print 'Subscribing to Redis channel F2B_CHANNEL' - while True: - for item in pubsub.listen(): - for rule_id, rule_regex in RULES.iteritems(): - if item['data'] and item['type'] == 'message': - result = re.search(rule_regex, item['data']) - if result: - addr = result.group(1) - ip = ipaddress.ip_address(addr.decode('ascii')) - if ip.is_private or ip.is_loopback: - continue - print '%s matched rule id %d' % (addr, rule_id) - log['time'] = int(round(time.time())) - log['priority'] = 'warn' - log['message'] = '%s matched rule id %d' % (addr, rule_id) - r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) - ban(addr) - -def autopurge(): - while not quit_now: - BAN_TIME = int(r.get('F2B_BAN_TIME')) - MAX_ATTEMPTS = int(r.get('F2B_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) - time.sleep(10) - -if __name__ == '__main__': - - watch_thread = Thread(target=watch) - watch_thread.daemon = True - watch_thread.start() - - autopurge_thread = Thread(target=autopurge) - autopurge_thread.daemon = True - autopurge_thread.start() - - signal.signal(signal.SIGTERM, quit) - atexit.register(clear) - - while not quit_now: - time.sleep(0.5) diff --git a/data/Dockerfiles/memcached/.empty b/data/Dockerfiles/memcached/.empty deleted file mode 100644 index e69de29b..00000000 diff --git a/data/Dockerfiles/mysql/.empty b/data/Dockerfiles/mysql/.empty deleted file mode 100644 index e69de29b..00000000 diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile new file mode 100644 index 00000000..fc75433e --- /dev/null +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:3.7 +LABEL maintainer "Andre Peters " + +RUN apk add -U python2 python-dev py-pip gcc musl-dev iptables ip6tables \ + && pip2 install --upgrade python-iptables redis ipaddress \ + && apk del python-dev py2-pip gcc + +COPY server.py / +CMD ["python2", "-u", "/server.py"] diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py new file mode 100644 index 00000000..5bb66547 --- /dev/null +++ b/data/Dockerfiles/netfilter/server.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python2 + +import re +import os +import time +import atexit +import signal +import ipaddress +import subprocess +from threading import Thread +import redis +import time +import json +import iptc + +r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0) +pubsub = r.pubsub() + +RULES = {} +RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' +RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' +RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' +RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' +RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' +RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' + +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 24 + f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 64 + r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False)) +else: + try: + f2boptions = {} + f2boptions = json.loads(r.get('F2B_OPTIONS')) + except ValueError, e: + print 'Error loading F2B options: F2B_OPTIONS is not json' + raise SystemExit(1) + +if r.exists('F2B_LOG'): + r.rename('F2B_LOG', 'NETFILTER_LOG') + +bans = {} +log = {} +quit_now = False + +def ban(address): + 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']) + WHITELIST = r.hgetall('F2B_WHITELIST') + + ip = ipaddress.ip_address(address.decode('ascii')) + 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.decode('ascii')) + if WHITELIST: + for wl_key in WHITELIST: + wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) + if wl_net.overlaps(self_network): + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print '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)).decode('ascii'), 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: + log['time'] = int(round(time.time())) + log['priority'] = 'crit' + log['message'] = 'Banning %s' % net + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Banning %s for %d minutes' % (net, BAN_TIME / 60) + if type(ip) is ipaddress.IPv4Address: + for c in ['INPUT', 'FORWARD']: + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c) + 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: + for c in ['INPUT', 'FORWARD']: + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c) + 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, log['time'] + BAN_TIME) + else: + log['time'] = int(round(time.time())) + log['priority'] = 'warn' + log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) + +def unban(net): + log['time'] = int(round(time.time())) + log['priority'] = 'info' + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + #if not net in bans: + # log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net + # r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + # print '%s is not banned, skipping unban and deleting from queue (if any)' % net + # r.hdel('F2B_QUEUE_UNBAN', '%s' % net) + # return + log['message'] = 'Unbanning %s' % net + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Unbanning %s' % net + if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: + for c in ['INPUT', 'FORWARD']: + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c) + rule = iptc.Rule() + rule.src = net + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule in chain.rules: + chain.delete_rule(rule) + else: + for c in ['INPUT', 'FORWARD']: + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c) + 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 quit(signum, frame): + global quit_now + quit_now = True + +def clear(): + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Clearing all bans' + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Clearing all bans' + for net in bans.copy(): + unban(net) + pubsub.unsubscribe() + +def watch(): + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Watching Redis channel F2B_CHANNEL' + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + pubsub.subscribe('F2B_CHANNEL') + print 'Subscribing to Redis channel F2B_CHANNEL' + while True: + for item in pubsub.listen(): + for rule_id, rule_regex in RULES.iteritems(): + if item['data'] and item['type'] == 'message': + result = re.search(rule_regex, item['data']) + if result: + addr = result.group(1) + ip = ipaddress.ip_address(addr.decode('ascii')) + if ip.is_private or ip.is_loopback: + continue + print '%s matched rule id %d' % (addr, rule_id) + log['time'] = int(round(time.time())) + log['priority'] = 'warn' + log['message'] = '%s matched rule id %d' % (addr, rule_id) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + ban(addr) + +def snat(snat_target): + def get_snat_rule(): + rule = iptc.Rule() + rule.position = 1 + 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 + return rule + + while True: + table = iptc.Table('nat') + table.autocommit = False + chain = iptc.Chain(table, 'POSTROUTING') + if get_snat_rule() not in chain.rules: + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Added POSTROUTING rule for source network ' + get_snat_rule().src + ' to SNAT target ' + snat_target + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print log['message'] + chain.insert_rule(get_snat_rule()) + table.commit() + table.refresh() + time.sleep(10) + +def autopurge(): + while not quit_now: + BAN_TIME = f2boptions['ban_time'] + MAX_ATTEMPTS = 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) + time.sleep(10) + +def cleanPrevious(): + print "Cleaning previously cached bans" + F2B_ACTIVE_BANS = r.hgetall('F2B_ACTIVE_BANS') + if F2B_ACTIVE_BANS: + for net in F2B_ACTIVE_BANS: + unban(str(net)) + +if __name__ == '__main__': + + cleanPrevious() + + watch_thread = Thread(target=watch) + watch_thread.daemon = True + watch_thread.start() + + if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n': + try: + snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii') + snat_ipo = ipaddress.ip_address(snat_ip) + if type(snat_ipo) is ipaddress.IPv4Address: + snat_thread = Thread(target=snat,args=(snat_ip,)) + snat_thread.daemon = True + snat_thread.start() + except ValueError: + print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address' + + autopurge_thread = Thread(target=autopurge) + autopurge_thread.daemon = True + autopurge_thread.start() + + signal.signal(signal.SIGTERM, quit) + atexit.register(clear) + + while not quit_now: + time.sleep(0.5) diff --git a/data/Dockerfiles/nginx/.empty b/data/Dockerfiles/nginx/.empty deleted file mode 100644 index e69de29b..00000000 diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 45d2fa84..00681ec2 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -33,6 +33,12 @@ RUN apk add -U --no-cache libxml2-dev \ imagemagick-dev \ imagemagick \ libtool \ + freetype \ + libpng \ + libjpeg-turbo \ + freetype-dev \ + libpng-dev \ + libjpeg-turbo-dev\ gettext-dev \ openldap-dev \ librsvg \ @@ -46,10 +52,33 @@ RUN apk add -U --no-cache libxml2-dev \ && docker-php-ext-enable redis apcu memcached imagick mailparse \ && pecl clear-cache \ && docker-php-ext-configure intl \ + && docker-php-ext-configure gd \ + --with-gd \ + --enable-gd-native-ttf \ + --with-freetype-dir=/usr/include/ \ + --with-png-dir=/usr/include/ \ + --with-jpeg-dir=/usr/include/ \ && docker-php-ext-install -j 4 intl gettext ldap sockets soap pdo pdo_mysql xmlrpc gd zip pcntl opcache \ && docker-php-ext-configure imap --with-imap --with-imap-ssl \ && docker-php-ext-install -j 4 imap \ - && apk del --purge autoconf g++ make libxml2-dev icu-dev imap-dev openssl-dev cyrus-sasl-dev pcre-dev libpng-dev libpng-dev libjpeg-turbo-dev libwebp-dev zlib-dev imagemagick-dev + && apk del --purge autoconf \ + g++ \ + make \ + libxml2-dev \ + icu-dev \ + imap-dev \ + openssl-dev \ + cyrus-sasl-dev \ + pcre-dev \ + libpng-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + zlib-dev \ + imagemagick-dev \ + freetype-dev \ + libpng-dev \ + libjpeg-turbo-dev COPY ./docker-entrypoint.sh / diff --git a/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/data/Dockerfiles/phpfpm/docker-entrypoint.sh index dc4197b4..8e8d507c 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -15,7 +15,6 @@ until [ $(redis-cli -h redis-mailcow PING) == "PONG" ]; do done # Migrate domain map - declare -a DOMAIN_ARR redis-cli -h redis-mailcow DEL DOMAIN_MAP while read line @@ -34,57 +33,4 @@ for domain in "${DOMAIN_ARR[@]}"; do done fi -# Migrate tag settings map - -declare -a SUBJ_TAG_ARR -redis-cli -h redis-mailcow DEL SUBJ_TAG_ARR -while read line -do - SUBJ_TAG_ARR+=("$line") -done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT username FROM mailbox WHERE wants_tagged_subject='1'" -Bs) - -if [[ ! -z ${SUBJ_TAG_ARR} ]]; then -for user in "${SUBJ_TAG_ARR[@]}"; do - redis-cli -h redis-mailcow HSET RCPT_WANTS_SUBJECT_TAG ${user} 1 - mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE mailbox SET wants_tagged_subject='2' WHERE username = '${user}'" -done -fi - -# Migrate DKIM keys - -for file in $(ls /data/dkim/keys/); do - domain=${file%.dkim} - if [[ -f /data/dkim/txt/${file} ]]; then - redis-cli -h redis-mailcow HSET DKIM_PUB_KEYS "${domain}" "$(cat /data/dkim/txt/${file})" - redis-cli -h redis-mailcow HSET DKIM_PRIV_KEYS "dkim.${domain}" "$(cat /data/dkim/keys/${file})" - redis-cli -h redis-mailcow HSET DKIM_SELECTORS "${domain}" "dkim" - fi - rm /data/dkim/{keys,txt}/${file} -done - -# Fix DKIM keys - -# Fetch domains -declare -a DOMAIN_ARRAY -while read line -do - DOMAIN_ARRAY+=("$line") -done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs) -while read line -do - DOMAIN_ARRAY+=("$line") -done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) - -# Loop through array and fix keys -if [[ ! -z ${DOMAIN_ARRAY} ]]; then - for domain in "${DOMAIN_ARRAY[@]}"; do - WRONG_KEY=$(redis-cli -h redis-mailcow HGET DKIM_PRIV_KEYS ${domain} | tr -d \") - if [[ ! -z ${WRONG_KEY} ]]; then - echo "Migrating defect key for domain ${domain}" - redis-cli -h redis-mailcow HSET DKIM_PRIV_KEYS "dkim.${domain}" ${WRONG_KEY} - redis-cli -h redis-mailcow HDEL DKIM_PRIV_KEYS "${domain}" - fi - done -fi - exec "$@" diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c152606d..489f676f 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -39,7 +39,7 @@ query = SELECT IF(EXISTS( SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d' ) - ) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1' + ) AND json_extract(attributes, '$.tls_enforce_in') LIKE '%%1%%' AND mailbox.active = '1' ), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF @@ -58,7 +58,7 @@ query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps WHERE alias_domain = '%d' ) ) - AND mailbox.tls_enforce_out = '1' + AND json_extract(attributes, '$.tls_enforce_out') LIKE '%%1%%' AND mailbox.active = '1' ), 'smtp_enforced_tls:', 'smtp:') AS 'transport' UNION ALL @@ -145,6 +145,16 @@ query = SELECT bcc_dest FROM bcc_maps AND active='1'; EOF +cat < /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT new_dest FROM recipient_maps + WHERE old_dest='%s' + AND active='1'; +EOF + cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf user = ${DBUSER} password = ${DBPASS} diff --git a/data/Dockerfiles/redis/.empty b/data/Dockerfiles/redis/.empty deleted file mode 100644 index e69de29b..00000000 diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index ae216570..15ae73da 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -1,5 +1,6 @@ #!/bin/bash chown -R _rspamd:_rspamd /var/lib/rspamd +[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Placeholder' > /etc/rspamd/override.d/worker-controller-password.inc exec /sbin/tini -- "$@" diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 1bfca949..bb429326 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -42,6 +42,9 @@ RUN mkdir /usr/share/doc/sogo \ COPY ./bootstrap-sogo.sh / COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY theme-blue.js /usr/lib/GNUstep/SOGo/WebServerResources/js/theme-blue.js +COPY theme-green.js /usr/lib/GNUstep/SOGo/WebServerResources/js/theme-green.js +COPY sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh index c360e30e..87bf05f7 100755 --- a/data/Dockerfiles/sogo/bootstrap-sogo.sh +++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -19,14 +19,13 @@ mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS so mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, home, kind, multiple_bookings) AS -SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gda.ad_alias, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox +SELECT mailbox.username, mailbox.domain, mailbox.username, if(json_extract(attributes, '$.force_pw_update') LIKE '%0%', password, 'invalid'), mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gda.ad_alias, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)') LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username WHERE mailbox.active = '1' GROUP BY mailbox.username; EOF - mkdir -p /var/lib/sogo/GNUstep/Defaults/ # Generate plist header with timezone data diff --git a/data/Dockerfiles/sogo/sogo-full.svg b/data/Dockerfiles/sogo/sogo-full.svg new file mode 100644 index 00000000..5e0d81df --- /dev/null +++ b/data/Dockerfiles/sogo/sogo-full.svg @@ -0,0 +1,48 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/data/Dockerfiles/sogo/theme-blue.js b/data/Dockerfiles/sogo/theme-blue.js new file mode 100644 index 00000000..4efbf824 --- /dev/null +++ b/data/Dockerfiles/sogo/theme-blue.js @@ -0,0 +1,60 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + 'use strict'; + + angular.module('SOGo.Common') + .config(configure) + + /** + * @ngInject + */ + configure.$inject = ['$mdThemingProvider']; + function configure($mdThemingProvider) { + + /** + * Define the Alternative theme + */ + $mdThemingProvider.theme('mailcow') + .primaryPalette('indigo', { + 'default': '700', // top toolbar + 'hue-1': '400', + 'hue-2': '600', // sidebar toolbar + 'hue-3': 'A700' + }) + .accentPalette('indigo', { + 'default': '500', // fab buttons + 'hue-1': '50', // center list toolbar + 'hue-2': '400', + 'hue-3': 'A700' + }) + .backgroundPalette('grey', { + 'default': '50', // center list background + 'hue-1': '100', + 'hue-2': '200', + 'hue-3': '300' + }); + $mdThemingProvider.theme('default') + .primaryPalette('indigo', { + 'default': '700', // top toolbar + 'hue-1': '400', + 'hue-2': '600', // sidebar toolbar + 'hue-3': 'A700' + }) + .accentPalette('indigo', { + 'default': '500', // fab buttons + 'hue-1': '50', // center list toolbar + 'hue-2': '400', + 'hue-3': 'A700' + }) + .backgroundPalette('grey', { + 'default': '50', // center list background + 'hue-1': '100', + 'hue-2': '200', + 'hue-3': '300' + }); + + $mdThemingProvider.setDefaultTheme('mailcow'); + $mdThemingProvider.generateThemesOnDemand(false); + } +})(); diff --git a/data/Dockerfiles/sogo/theme-green.js b/data/Dockerfiles/sogo/theme-green.js new file mode 100644 index 00000000..35a6f788 --- /dev/null +++ b/data/Dockerfiles/sogo/theme-green.js @@ -0,0 +1,61 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + 'use strict'; + + angular.module('SOGo.Common') + .config(configure) + + /** + * @ngInject + */ + configure.$inject = ['$mdThemingProvider']; + function configure($mdThemingProvider) { + + + /** + * Define the Alternative theme + */ + $mdThemingProvider.theme('mailcow') + .primaryPalette('green', { + 'default': '600', // top toolbar + 'hue-1': '200', + 'hue-2': '600', // sidebar toolbar + 'hue-3': 'A700' + }) + .accentPalette('green', { + 'default': '600', // fab buttons + 'hue-1': '50', // center list toolbar + 'hue-2': '400', + 'hue-3': 'A700' + }) + .backgroundPalette('grey', { + 'default': '50', // center list background + 'hue-1': '50', + 'hue-2': '100', + 'hue-3': '100' + }); + $mdThemingProvider.theme('default') + .primaryPalette('green', { + 'default': '600', // top toolbar + 'hue-1': '200', + 'hue-2': '600', // sidebar toolbar + 'hue-3': 'A700' + }) + .accentPalette('green', { + 'default': '600', // fab buttons + 'hue-1': '50', // center list toolbar + 'hue-2': '400', + 'hue-3': 'A700' + }) + .backgroundPalette('grey', { + 'default': '50', // center list background + 'hue-1': '50', + 'hue-2': '100', + 'hue-3': '100' + }); + + $mdThemingProvider.setDefaultTheme('mailcow'); + $mdThemingProvider.generateThemesOnDemand(false); + } +})(); diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index bed11907..39760ec0 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -211,7 +211,7 @@ rspamd_checks() { while [ ${err_count} -lt ${THRESHOLD} ]; do host_ip=$(get_container_ip rspamd-mailcow) err_c_cur=${err_count} - SCORE=$(curl --silent ${host_ip}:11333/scan -d ' + SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/scan -d ' To: null@localhost From: watchdog@localhost diff --git a/data/assets/nextcloud/nextcloud.conf b/data/assets/nextcloud/nextcloud.conf index 2fa9d9d8..4759bff2 100644 --- a/data/assets/nextcloud/nextcloud.conf +++ b/data/assets/nextcloud/nextcloud.conf @@ -24,6 +24,7 @@ server { add_header X-Robots-Tag none; add_header X-Download-Options noopen; add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options "SAMEORIGIN"; server_name NC_SUBD; @@ -54,7 +55,7 @@ server { gzip_min_length 256; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; - set_real_ip_from fd00::/8; + set_real_ip_from fc00::/7; set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; diff --git a/data/assets/nextcloud/site.nextcloud.custom b/data/assets/nextcloud/site.nextcloud.custom index 6901df76..f7d6dae0 100644 --- a/data/assets/nextcloud/site.nextcloud.custom +++ b/data/assets/nextcloud/site.nextcloud.custom @@ -33,6 +33,7 @@ add_header X-Robots-Tag none; add_header X-Download-Options noopen; add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options "SAMEORIGIN"; access_log off; } location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ { diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index d35f0186..e0b95531 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -174,7 +174,7 @@ namespace { type = shared separator = / prefix = Shared/%%u/ - location = maildir:%%h/:INDEXPVT=~/Shared/%%u + location = maildir:%%h/:INDEX=~/Shared/%%u:INDEXPVT=~/Shared/%%u subscriptions = no list = children } @@ -239,8 +239,10 @@ userdb { driver = sql } protocol imap { + imap_metadata = yes mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape #mail_crypt } +mail_attribute_dict = file:%h/dovecot-attributes protocol lmtp { mail_plugins = quota sieve acl zlib listescape #mail_crypt auth_socket_path = /usr/local/var/run/dovecot/auth-master @@ -280,6 +282,8 @@ plugin { #mail_crypt_global_private_key = - rcpt = ""; + rcpt = ; prepare("SELECT `option`, `value` FROM `filterconf` @@ -172,7 +172,7 @@ while ($row = array_shift($rows)) { - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; - rcpt = ""; + rcpt = ; &$goto) { - error_log("quarantaine pipe: query " . $goto . " as username from mailbox"); + error_log("quarantine pipe: query " . $goto . " as username from mailbox"); $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND `active`= '1';"); $stmt->execute(array(':goto' => $goto)); $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; if (!empty($username)) { - error_log("quarantaine pipe: mailbox found: " . $username); + error_log("quarantine pipe: mailbox found: " . $username); // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate if (!in_array($username, $rcpt_final_mailboxes)) { $rcpt_final_mailboxes[] = $username; @@ -153,7 +153,7 @@ foreach (json_decode($rcpts, true) as $rcpt) { $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); $stmt->execute(array(':goto' => $goto)); $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; - error_log("quarantaine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); + error_log("quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); $goto_branch_array = explode(',', $goto_branch); } } @@ -173,7 +173,7 @@ foreach (json_decode($rcpts, true) as $rcpt) { // Force exit if loop cannot be solved // Postfix does not allow for alias loops, so this should never happen. $loop_c++; - error_log("quarantaine pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array)); + error_log("quarantine pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array)); } } catch (PDOException $e) { @@ -184,9 +184,9 @@ foreach (json_decode($rcpts, true) as $rcpt) { } foreach ($rcpt_final_mailboxes as $rcpt) { - error_log("quarantaine pipe: processing quarantaine message for rcpt " . $rcpt); + error_log("quarantine pipe: processing quarantine message for rcpt " . $rcpt); try { - $stmt = $pdo->prepare("INSERT INTO `quarantaine` (`qid`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`) + $stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`) VALUES (:qid, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action)"); $stmt->execute(array( ':qid' => $qid, @@ -199,11 +199,11 @@ foreach ($rcpt_final_mailboxes as $rcpt) { ':msg' => $raw_data, ':action' => $action )); - $stmt = $pdo->prepare('DELETE FROM `quarantaine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( + $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( SELECT `id` FROM ( SELECT `id` - FROM `quarantaine` + FROM `quarantine` WHERE `rcpt` = :rcpt2 ORDER BY id DESC LIMIT :retention_size diff --git a/data/conf/rspamd/override.d/worker-controller-password.inc b/data/conf/rspamd/override.d/worker-controller-password.inc index dcf2c804..9a5984d1 100644 --- a/data/conf/rspamd/override.d/worker-controller-password.inc +++ b/data/conf/rspamd/override.d/worker-controller-password.inc @@ -1 +1 @@ -# Placeholder +# Placeholder diff --git a/data/conf/rspamd/override.d/worker-controller.inc b/data/conf/rspamd/override.d/worker-controller.inc index 7f17485d..60338e15 100644 --- a/data/conf/rspamd/override.d/worker-controller.inc +++ b/data/conf/rspamd/override.d/worker-controller.inc @@ -1,9 +1,7 @@ bind_socket = "*:11334"; -secure_ip = "192.168.0.0/16"; -secure_ip = "172.16.0.0/12"; -secure_ip = "10.0.0.0/8"; +count = 2; secure_ip = "127.0.0.1"; secure_ip = "::1"; -secure_ip = "fd00::/8" +bind_socket = "/rspamd-sock/rspamd.sock mode=0666 owner=nobody"; .include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc" .include(try=true; priority=20) "$CONFDIR/override.d/worker-controller.custom.inc" diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf index 0acb8a80..40e3928c 100644 --- a/data/conf/sogo/sogo.conf +++ b/data/conf/sogo/sogo.conf @@ -5,7 +5,7 @@ PrivateDAndTViewer ); - WOWorkersCount = "7"; + WOWorkersCount = "14"; SOGoACLsSendEMailNotifications = YES; SOGoAppointmentSendEMailNotifications = YES; SOGoDraftsFolderName = "Drafts"; @@ -14,6 +14,7 @@ SOGoEnableEMailAlarms = NO; SOGoFoldersSendEMailNotifications = YES; SOGoForwardEnabled = YES; + SOGoUIAdditionalJSFiles = (js/theme-blue.js); // Multi-domain setup // Domains are isolated, you can define visibility options here. diff --git a/data/conf/unbound/unbound.conf b/data/conf/unbound/unbound.conf index 16952ff2..b3c77671 100644 --- a/data/conf/unbound/unbound.conf +++ b/data/conf/unbound/unbound.conf @@ -11,7 +11,7 @@ server: access-control: 10.0.0.0/8 allow access-control: 172.16.0.0/12 allow access-control: 192.168.0.0/16 allow - access-control: fd00::/8 allow + access-control: fc00::/7 allow access-control: fe80::/10 allow directory: "/etc/unbound" username: unbound @@ -20,7 +20,7 @@ server: private-address: 172.16.0.0/12 private-address: 192.168.0.0/16 private-address: 169.254.0.0/16 - private-address: fd00::/8 + private-address: fc00::/7 private-address: fe80::/10 root-hints: "/etc/unbound/root.hints" hide-identity: yes diff --git a/data/web/admin.php b/data/web/admin.php index 0d4affd0..06881fa0 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -147,7 +147,7 @@ $tfa_data = get_tfa(); Relayhosts - + @@ -414,28 +414,28 @@ $tfa_data = get_tfa(); - +
-
+
- -
+ +
- +
- +
-
+
- +
diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index 4c71a295..557c887c 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -36,9 +36,8 @@ $opt = [ $pdo = new PDO($dsn, $database_user, $database_pass, $opt); $login_user = strtolower(trim($_SERVER['PHP_AUTH_USER'])); $login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW'])); -$login_role = check_login($login_user, $login_pass); -if (!isset($_SERVER['PHP_AUTH_USER']) OR $login_role !== "user") { +if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) { try { $json = json_encode( array( @@ -62,35 +61,36 @@ if (!isset($_SERVER['PHP_AUTH_USER']) OR $login_role !== "user") { header('HTTP/1.0 401 Unauthorized'); exit(0); } -else { - if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { - if ($login_role === "user") { - header("Content-Type: application/xml"); - echo '' . PHP_EOL; + +$login_role = check_login($login_user, $login_pass); + +if ($login_role === "user") { + header("Content-Type: application/xml"); + echo '' . PHP_EOL; ?> time(), - "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => $_SERVER['PHP_AUTH_USER'], - "service" => "Error: invalid or missing request data" - ) - ); - $redis->lPush('AUTODISCOVER_LOG', $json); - $redis->lTrim('AUTODISCOVER_LOG', 0, 100); - } - catch (RedisException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'Redis: '.$e - ); - return false; - } - list($usec, $sec) = explode(' ', microtime()); + if(!$data) { + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $_SERVER['PHP_AUTH_USER'], + "service" => "Error: invalid or missing request data" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + list($usec, $sec) = explode(' ', microtime()); ?> @@ -101,54 +101,54 @@ else { Request->EMailAddress; - } catch (Exception $e) { - $email = $_SERVER['PHP_AUTH_USER']; - } + exit(0); + } + try { + $discover = new SimpleXMLElement($data); + $email = $discover->Request->EMailAddress; + } catch (Exception $e) { + $email = $_SERVER['PHP_AUTH_USER']; + } - $username = trim($email); - try { - $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = $MailboxData['name']; - } - else { - $displayname = $email; - } - try { - $json = json_encode( - array( - "time" => time(), - "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => $_SERVER['PHP_AUTH_USER'], - "service" => $autodiscover_config['autodiscoverType'] - ) - ); - $redis->lPush('AUTODISCOVER_LOG', $json); - $redis->lTrim('AUTODISCOVER_LOG', 0, 100); - } - catch (RedisException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'Redis: '.$e - ); - return false; - } - if ($autodiscover_config['autodiscoverType'] == 'imap') { + $username = trim($email); + try { + $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + die("Failed to determine name from SQL"); + } + if (!empty($MailboxData['name'])) { + $displayname = $MailboxData['name']; + } + else { + $displayname = $email; + } + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $_SERVER['PHP_AUTH_USER'], + "service" => $autodiscover_config['autodiscoverType'] + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + if ($autodiscover_config['autodiscoverType'] == 'imap') { ?> - + email @@ -190,13 +190,13 @@ else { en:en - + @@ -210,11 +210,9 @@ else { diff --git a/data/web/css/debug.css b/data/web/css/debug.css index 585d1905..e46a5b07 100644 --- a/data/web/css/debug.css +++ b/data/web/css/debug.css @@ -34,4 +34,4 @@ table.footable>tbody>tr.footable-empty>td { } .inputMissingAttr { border-color: #FF4136; -} \ No newline at end of file +} diff --git a/data/web/css/quarantaine.css b/data/web/css/quarantine.css similarity index 90% rename from data/web/css/quarantaine.css rename to data/web/css/quarantine.css index 7a5ee761..6690ab90 100644 --- a/data/web/css/quarantaine.css +++ b/data/web/css/quarantine.css @@ -28,10 +28,10 @@ table.footable>tbody>tr.footable-empty>td { width: 80%; } } -.mass-actions-quarantaine { +.mass-actions-quarantine { user-select: none; padding:10px 0 10px 10px; } .inputMissingAttr { border-color: #FF4136; -} \ No newline at end of file +} diff --git a/data/web/debug.php b/data/web/debug.php index 0723ba6a..09616791 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -9,21 +9,14 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
-
+ 'df', 'dir' => '/var/vmail'); + $vmail_df = explode(',', json_decode(docker('dovecot-mailcow', 'post', 'exec', $exec_fields), true)); + ?> +
-

Rspamd UI

+

Disk usage

-
-
-
-
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
-
- Rspamd UI +

/var/vmail on

+

/ ()

+
+
+
+
+
-
- -
-
-
-

Rspamd settings map

-
-
- -
-
-
- -

Container information

@@ -112,7 +80,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; 'redis-mailcow', 'php-fpm-mailcow', 'mysql-mailcow', - 'fail2ban-mailcow', + 'netfilter-mailcow', 'clamd-mailcow' ); foreach ($container_array as $container) { @@ -123,21 +91,26 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; setTimestamp(mktime( - $StartedAt['hour'], - $StartedAt['minute'], - $StartedAt['second'], - $StartedAt['month'], - $StartedAt['day'], - $StartedAt['year'])); - $user_tz = new DateTimeZone(getenv('TZ')); - $date->setTimezone($user_tz); - $started = $date->format('r'); + if ($StartedAt['hour'] !== false) { + $date = new \DateTime(); + $date->setTimestamp(mktime( + $StartedAt['hour'], + $StartedAt['minute'], + $StartedAt['second'], + $StartedAt['month'], + $StartedAt['day'], + $StartedAt['year'])); + $user_tz = new DateTimeZone(getenv('TZ')); + $date->setTimezone($user_tz); + $started = $date->format('r'); + } + else { + $started = '?'; + } ?> (Started on ), Restart -     +    
-
+
-
Fail2ban +
Netfilter
- - - + + +
-
+
@@ -300,6 +273,60 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+
+
+
+

Rspamd UI

+
+
+
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ Rspamd UI +
+
+
+
+
+ +
+
+
+

Rspamd settings map

+
+
+ +
+
+
+
diff --git a/data/web/edit.php b/data/web/edit.php index 8374dc76..b2f6c27e 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -20,7 +20,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") { if (isset($_GET["alias"]) && !empty($_GET["alias"])) { - $alias = $_GET["alias"]; + $alias = html_entity_decode(rawurldecode($_GET["alias"])); $result = mailbox('get', 'alias_details', $alias); if (!empty($result)) { ?> @@ -46,7 +46,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -313,9 +313,9 @@ if (isset($_SESSION['mailcow_cc_role'])) { } } elseif (isset($_GET['aliasdomain']) && - is_valid_domain_name($_GET["aliasdomain"]) && + is_valid_domain_name(html_entity_decode(rawurldecode($_GET["aliasdomain"]))) && !empty($_GET["aliasdomain"])) { - $alias_domain = $_GET["aliasdomain"]; + $alias_domain = html_entity_decode(rawurldecode($_GET["aliasdomain"])); $result = mailbox('get', 'alias_domain_details', $alias_domain); $rl = mailbox('get', 'ratelimit', $alias_domain); if (!empty($result)) { @@ -380,8 +380,8 @@ if (isset($_SESSION['mailcow_cc_role'])) { +
@@ -476,6 +477,14 @@ if (isset($_SESSION['mailcow_cc_role'])) {
+
+
+
+ + +
+
+
@@ -500,7 +509,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -555,8 +564,8 @@ if (isset($_SESSION['mailcow_cc_role'])) { @@ -621,7 +630,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- BCC destinations can only be valid email addresses. Separated by whitespace, semicolon, new line or comma. + BCC destination must be a single valid email address.
@@ -654,6 +663,43 @@ if (isset($_SESSION['mailcow_cc_role'])) { +

Recipient map:

+
+
+ +
+ +
+ + Recipient map destinations can only be valid email addresses. Separated by whitespace, semicolon, new line or comma. +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + + + +image/svg+xml \ No newline at end of file diff --git a/data/web/inc/ajax/destroy_tfa_auth.php b/data/web/inc/ajax/destroy_tfa_auth.php new file mode 100644 index 00000000..72c7f1e3 --- /dev/null +++ b/data/web/inc/ajax/destroy_tfa_auth.php @@ -0,0 +1,6 @@ + diff --git a/data/web/inc/ajax/qitem_details.php b/data/web/inc/ajax/qitem_details.php index fc6434ed..801fd3d0 100644 --- a/data/web/inc/ajax/qitem_details.php +++ b/data/web/inc/ajax/qitem_details.php @@ -23,7 +23,7 @@ function rrmdir($src) { } if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) { $tmpdir = '/tmp/' . $_GET['id'] . '/'; - $mailc = quarantaine('details', $_GET['id']); + $mailc = quarantine('details', $_GET['id']); if (strlen($mailc['msg']) > 10485760) { echo json_encode(array('error' => 'Message size exceeds 10 MiB.')); exit; diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 3c92c28e..1015c114 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -61,7 +61,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "/api/v1/get/u2f-authentication/", + url: "/api/v1/get/u2f-authentication/", complete: function(data){ $('#u2f_status_auth').html(''); data; @@ -79,6 +79,17 @@ $(document).ready(function() { }); } }); + $('#ConfirmTFAModal').on('hidden.bs.modal', function(){ + $.ajax({ + type: "GET", + cache: false, + dataType: 'script', + url: '/inc/ajax/destroy_tfa_auth.php', + complete: function(data){ + window.location = window.location.href.split("#")[0]; + } + }); + }); // Set TFA modals @@ -100,7 +111,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "/api/v1/get/u2f-registration/", + url: "/api/v1/get/u2f-registration/", complete: function(data){ data; setTimeout(function() { @@ -205,7 +216,7 @@ $(document).ready(function() { $('#triggerRestartContainer').html(' '); $('#statusTriggerRestartContainer2').append(data); $('#triggerRestartContainer').html(' '); - location.reload(); + window.location = window.location.href.split("#")[0]; } }); }); diff --git a/data/web/inc/functions.bcc.inc.php b/data/web/inc/functions.address_rewriting.inc.php similarity index 52% rename from data/web/inc/functions.bcc.inc.php rename to data/web/inc/functions.address_rewriting.inc.php index 1ce58a54..b731e16b 100644 --- a/data/web/inc/functions.bcc.inc.php +++ b/data/web/inc/functions.address_rewriting.inc.php @@ -15,7 +15,7 @@ function bcc($_action, $_data = null, $attr = null) { return false; } $local_dest = strtolower(trim($_data['local_dest'])); - $bcc_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['bcc_dest'])); + $bcc_dest = $_data['bcc_dest']; $active = intval($_data['active']); $type = $_data['type']; if ($type != 'sender' && $type != 'rcpt') { @@ -60,18 +60,10 @@ function bcc($_action, $_data = null, $attr = null) { else { return false; } - foreach ($bcc_dest as &$bcc_dest_e) { - if (!filter_var($bcc_dest_e, FILTER_VALIDATE_EMAIL)) { - $bcc_dest_e = null;; - } - $bcc_dest_e = strtolower($bcc_dest_e); - } - $bcc_dest = array_filter($bcc_dest); - $bcc_dest = implode(",", $bcc_dest); - if (empty($bcc_dest)) { + if (!filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) { $_SESSION['return'] = array( 'type' => 'danger', - 'msg' => 'BCC map destination cannot be empty' + 'msg' => 'BCC map must be a valid email address' ); return false; } @@ -142,16 +134,14 @@ function bcc($_action, $_data = null, $attr = null) { ); return false; } - $bcc_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $bcc_dest)); $active = intval($_data['active']); - foreach ($bcc_dest as &$bcc_dest_e) { - if (!filter_var($bcc_dest_e, FILTER_VALIDATE_EMAIL)) { - $bcc_dest_e = null;; - } - $bcc_dest_e = strtolower($bcc_dest_e); + if (!filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'BCC map must be a valid email address' + ); + return false; } - $bcc_dest = array_filter($bcc_dest); - $bcc_dest = implode(",", $bcc_dest); if (empty($bcc_dest)) { $_SESSION['return'] = array( 'type' => 'danger', @@ -289,4 +279,237 @@ function bcc($_action, $_data = null, $attr = null) { return true; break; } -} \ No newline at end of file +} + +function recipient_map($_action, $_data = null, $attr = null) { + global $pdo; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + return false; + } + switch ($_action) { + case 'add': + $old_dest = strtolower(trim($_data['recipient_map_old'])); + $new_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['recipient_map_new'])); + $active = intval($_data['active']); + if (empty($new_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Recipient map destination cannot be empty' + ); + return false; + } + if (is_valid_domain_name($old_dest)) { + $old_dest_sane = '@' . idn_to_ascii($old_dest); + } + elseif (filter_var($old_dest, FILTER_VALIDATE_EMAIL)) { + $old_dest_sane = $old_dest; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid original recipient specified: ' . $old_dest + ); + return false; + } + foreach ($new_dest as &$new_dest_e) { + if (!filter_var($new_dest_e, FILTER_VALIDATE_EMAIL)) { + $new_dest_e = null;; + } + $new_dest_e = strtolower($new_dest_e); + } + $new_dest = array_filter($new_dest); + $new_dest = implode(",", $new_dest); + if (empty($new_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Recipient map destination cannot be empty' + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `id` FROM `recipient_maps` + WHERE `old_dest` = :old_dest"); + $stmt->execute(array(':old_dest' => $old_dest_sane)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'A Recipient map entry "' . htmlspecialchars($old_dest_sane) . '" exists' + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `recipient_maps` (`old_dest`, `new_dest`, `active`) VALUES + (:old_dest, :new_dest, :active)"); + $stmt->execute(array( + ':old_dest' => $old_dest_sane, + ':new_dest' => $new_dest, + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Recipient map entry saved' + ); + break; + case 'edit': + $ids = (array)$_data['id']; + foreach ($ids as $id) { + $is_now = recipient_map('details', $id); + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $new_dest = (!empty($_data['recipient_map_new'])) ? $_data['recipient_map_new'] : $is_now['recipient_map_new']; + $old_dest = $is_now['old_dest']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $new_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $new_dest)); + $active = intval($_data['active']); + foreach ($new_dest as &$new_dest_e) { + if (!filter_var($new_dest_e, FILTER_VALIDATE_EMAIL)) { + $new_dest_e = null;; + } + $new_dest_e = strtolower($new_dest_e); + } + $new_dest = array_filter($new_dest); + $new_dest = implode(",", $new_dest); + if (empty($new_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Recipient map destination cannot be empty' + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `id` FROM `recipient_maps` + WHERE `old_dest` = :old_dest"); + $stmt->execute(array(':old_dest' => $old_dest)); + $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id']; + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if (isset($id_now) && $id_now != $id) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'A Recipient map entry ' . htmlspecialchars($old_dest) . ' exists' + ); + return false; + } + try { + $stmt = $pdo->prepare("UPDATE `recipient_maps` SET `new_dest` = :new_dest, `active` = :active WHERE `id`= :id"); + $stmt->execute(array( + ':new_dest' => $new_dest, + ':active' => $active, + ':id' => $id + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Recipient map entry edited' + ); + break; + case 'details': + $mapdata = array(); + $id = intval($_data); + try { + $stmt = $pdo->prepare("SELECT `id`, + `old_dest` AS `recipient_map_old`, + `new_dest` AS `recipient_map_new`, + `active` AS `active_int`, + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`, + `created`, + `modified` FROM `recipient_maps` + WHERE `id` = :id"); + $stmt->execute(array(':id' => $id)); + $mapdata = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + return $mapdata; + break; + case 'get': + $mapdata = array(); + $all_items = array(); + $id = intval($_data); + try { + $stmt = $pdo->query("SELECT `id` FROM `recipient_maps`"); + $all_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + foreach ($all_items as $i) { + $mapdata[] = $i['id']; + } + $all_items = null; + return $mapdata; + break; + case 'delete': + $ids = (array)$_data['id']; + foreach ($ids as $id) { + if (!is_numeric($id)) { + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `recipient_maps` WHERE `id`= :id"); + $stmt->execute(array(':id' => $id)); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Deleted Recipient map id/s ' . implode(', ', $ids) + ); + return true; + break; + } +} diff --git a/data/web/inc/functions.docker.inc.php b/data/web/inc/functions.docker.inc.php index 5b6afa9f..11747f25 100644 --- a/data/web/inc/functions.docker.inc.php +++ b/data/web/inc/functions.docker.inc.php @@ -7,6 +7,7 @@ function docker($service_name, $action, $attr1 = null, $attr2 = null, $extra_hea curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/json'); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 0); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); $response = curl_exec($curl); if ($response === false) { $err = curl_error($curl); @@ -32,6 +33,7 @@ function docker($service_name, $action, $attr1 = null, $attr2 = null, $extra_hea curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/json'); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 0); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); $response = curl_exec($curl); if ($response === false) { $err = curl_error($curl); @@ -58,6 +60,7 @@ function docker($service_name, $action, $attr1 = null, $attr2 = null, $extra_hea if (ctype_xdigit($container_id) && ctype_alnum($attr1)) { curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $attr1); curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); if (!empty($attr2)) { curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($attr2)); } diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index b78fb36a..5acbf60f 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -4,30 +4,26 @@ function fail2ban($_action, $_data = null) { global $lang; switch ($_action) { case 'get': - $data = array(); + $f2b_options = array(); if ($_SESSION['mailcow_cc_role'] != "admin") { return false; } try { - $data['ban_time'] = $redis->Get('F2B_BAN_TIME'); - $data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS'); - $data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW'); - $data['netban_ipv4'] = $redis->Get('F2B_NETBAN_IPV4'); - $data['netban_ipv6'] = $redis->Get('F2B_NETBAN_IPV6'); + $f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true); $wl = $redis->hGetAll('F2B_WHITELIST'); if (is_array($wl)) { foreach ($wl as $key => $value) { $tmp_data[] = $key; } if (isset($tmp_data)) { - $data['whitelist'] = implode(PHP_EOL, $tmp_data); + $f2b_options['whitelist'] = implode(PHP_EOL, $tmp_data); } else { - $data['whitelist'] = ""; + $f2b_options['whitelist'] = ""; } } else { - $data['whitelist'] = ""; + $f2b_options['whitelist'] = ""; } } catch (RedisException $e) { @@ -37,7 +33,7 @@ function fail2ban($_action, $_data = null) { ); return false; } - return $data; + return $f2b_options; break; case 'edit': if ($_SESSION['mailcow_cc_role'] != "admin") { @@ -63,21 +59,16 @@ function fail2ban($_action, $_data = null) { return false; } $wl = $_data['whitelist']; - $ban_time = ($ban_time < 60) ? 60 : $ban_time; - - $netban_ipv4 = ($netban_ipv4 < 8) ? 8 : $netban_ipv4; - $netban_ipv6 = ($netban_ipv6 < 8) ? 8 : $netban_ipv6; - $netban_ipv4 = ($netban_ipv4 > 32) ? 32 : $netban_ipv4; - $netban_ipv6 = ($netban_ipv6 > 128) ? 128 : $netban_ipv6; - - $max_attempts = ($max_attempts < 1) ? 1 : $max_attempts; - $retry_window = ($retry_window < 1) ? 1 : $retry_window; + $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_BAN_TIME', $ban_time); - $redis->Set('F2B_MAX_ATTEMPTS', $max_attempts); - $redis->Set('F2B_RETRY_WINDOW', $retry_window); - $redis->Set('F2B_NETBAN_IPV4', $netban_ipv4); - $redis->Set('F2B_NETBAN_IPV6', $netban_ipv6); + $redis->Set('F2B_OPTIONS', json_encode($f2b_options)); $redis->Del('F2B_WHITELIST'); if(!empty($wl)) { $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl)); diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index f7f08403..6876c0d2 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -39,7 +39,7 @@ function hasDomainAccess($username, $role, $domain) { } function hasMailboxObjectAccess($username, $role, $object) { global $pdo; - if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { + 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') { @@ -74,7 +74,7 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { return "Not a valid hostname"; } if (empty($starttls)) { - $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); + $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 : '-'; @@ -112,6 +112,7 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { 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); @@ -414,7 +415,7 @@ function edit_user_account($postarray) { } $password_hashed = hash_password($password_new); try { - $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed, `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0') WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, ':username' => $username @@ -470,22 +471,18 @@ function user_get_alias_details($username) { )); $run = $stmt->fetchAll(PDO::FETCH_ASSOC); while ($row = array_shift($run)) { - $data['direct_aliases'] = $row['direct_aliases']; + $data['direct_aliases'][] = $row['direct_aliases']; } - $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ', '), '✘') AS `ad_alias` FROM `mailbox` + $stmt = $pdo->prepare("SELECT GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ', ') AS `ad_alias` 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($data['direct_aliases'])) { - $data['direct_aliases'] = $row['ad_alias']; - } - else { - // Probably faster than imploding - $data['direct_aliases'] .= ', ' . $row['ad_alias']; - } + $data['direct_aliases'][] = $row['ad_alias']; } + $data['direct_aliases'] = implode(', ', array_filter($data['direct_aliases'])); + $data['direct_aliases'] = empty($data['direct_aliases']) ? '✘' : $data['direct_aliases']; $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); @@ -1134,13 +1131,13 @@ function get_logs($container, $lines = false) { return $data_array; } } - if ($container == "fail2ban-mailcow") { + if ($container == "netfilter-mailcow") { if (!is_numeric($lines)) { list ($from, $to) = explode('-', $lines); - $data = $redis->lRange('F2B_LOG', intval($from), intval($to)); + $data = $redis->lRange('NETFILTER_LOG', intval($from), intval($to)); } else { - $data = $redis->lRange('F2B_LOG', 0, intval($lines)); + $data = $redis->lRange('NETFILTER_LOG', 0, intval($lines)); } if ($data) { foreach ($data as $json_line) { @@ -1166,12 +1163,13 @@ function get_logs($container, $lines = false) { } if ($container == "rspamd-history") { $curl = curl_init(); + curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/rspamd-sock/rspamd.sock'); if (!is_numeric($lines)) { list ($from, $to) = explode('-', $lines); - curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history?from=" . intval($from) . "&to=" . intval($to)); + curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to)); } else { - curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history?to=" . intval($lines)); + curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines)); } curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); $history = curl_exec($curl); diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 5bd9ac58..ff6a56d5 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -891,8 +891,8 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `active`) - VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :active)"); + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `attributes`, `active`) + VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, '{\"force_pw_update\": \"0\", \"tls_enforce_in\": \"0\", \"tls_enforce_out\": \"0\"}', :active)"); $stmt->execute(array( ':username' => $username, ':password_hashed' => $password_hashed, @@ -1152,10 +1152,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { return false; } try { - $stmt = $pdo->prepare("UPDATE `mailbox` SET `tls_enforce_out` = :tls_out, `tls_enforce_in` = :tls_in WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_out), `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_in) WHERE `username` = :username"); $stmt->execute(array( - ':tls_out' => $tls_enforce_out, - ':tls_in' => $tls_enforce_in, + ':tls_out' => intval($tls_enforce_out), + ':tls_in' => intval($tls_enforce_in), ':username' => $username )); } @@ -1954,6 +1954,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $is_now = mailbox('get', 'mailbox_details', $username); if (!empty($is_now)) { $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); $name = (!empty($_data['name'])) ? $_data['name'] : $is_now['name']; $domain = $is_now['domain']; $quota_m = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['quota'] / 1048576); @@ -2113,24 +2114,11 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { } $password_hashed = hash_password($password); try { - $stmt = $pdo->prepare("UPDATE `alias` SET - `active` = :active - WHERE `address` = :address"); - $stmt->execute(array( - ':address' => $username, - ':active' => $active - )); $stmt = $pdo->prepare("UPDATE `mailbox` SET - `active` = :active, - `password` = :password_hashed, - `name`= :name, - `quota` = :quota_b + `password` = :password_hashed WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':active' => $active, - ':name' => $name, - ':quota_b' => $quota_b, ':username' => $username )); } @@ -2153,12 +2141,14 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $stmt = $pdo->prepare("UPDATE `mailbox` SET `active` = :active, `name`= :name, - `quota` = :quota_b + `quota` = :quota_b, + `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update) WHERE `username` = :username"); $stmt->execute(array( ':active' => $active, ':name' => $name, ':quota_b' => $quota_b, + ':force_pw_update' => $force_pw_update, ':username' => $username )); } @@ -2402,7 +2392,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { return $mailboxes; break; case 'tls_policy': - $policydata = array(); + $attrs = array(); if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { return false; @@ -2412,9 +2402,9 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $_data = $_SESSION['mailcow_cc_username']; } try { - $stmt = $pdo->prepare("SELECT `tls_enforce_out`, `tls_enforce_in` FROM `mailbox` WHERE `username` = :username"); + $stmt = $pdo->prepare("SELECT `attributes` FROM `mailbox` WHERE `username` = :username"); $stmt->execute(array(':username' => $_data)); - $policydata = $stmt->fetch(PDO::FETCH_ASSOC); + $attrs = $stmt->fetch(PDO::FETCH_ASSOC); } catch(PDOException $e) { $_SESSION['return'] = array( @@ -2423,7 +2413,11 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { ); return false; } - return $policydata; + $attrs = json_decode($attrs['attributes'], true); + return array( + 'tls_enforce_in' => $attrs['tls_enforce_in'], + 'tls_enforce_out' => $attrs['tls_enforce_out'] + ); break; case 'filters': $filters = array(); @@ -3070,6 +3064,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { `mailbox`.`domain`, `mailbox`.`quota`, `quota2`.`bytes`, + `attributes`, `quota2`.`messages` FROM `mailbox`, `quota2`, `domain` WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group' AND `mailbox`.`username` = `quota2`.`username` AND `domain`.`domain` = `mailbox`.`domain` AND `mailbox`.`username` = :mailbox"); @@ -3097,6 +3092,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $mailboxdata['active_int'] = $row['active_int']; $mailboxdata['domain'] = $row['domain']; $mailboxdata['quota'] = $row['quota']; + $mailboxdata['attributes'] = json_decode($row['attributes'], true); $mailboxdata['quota_used'] = intval($row['bytes']); $mailboxdata['percent_in_use'] = round((intval($row['bytes']) / intval($row['quota'])) * 100); $mailboxdata['messages'] = $row['messages']; diff --git a/data/web/inc/functions.policy.inc.php b/data/web/inc/functions.policy.inc.php index 9609d5e1..63f178c1 100644 --- a/data/web/inc/functions.policy.inc.php +++ b/data/web/inc/functions.policy.inc.php @@ -94,7 +94,7 @@ function policy($_action, $_scope, $_data = null) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) { $_SESSION['return'] = array( 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) + 'msg' => $object ); return false; } diff --git a/data/web/inc/functions.quarantaine.inc.php b/data/web/inc/functions.quarantine.inc.php similarity index 87% rename from data/web/inc/functions.quarantaine.inc.php rename to data/web/inc/functions.quarantine.inc.php index 15b8b94f..057ebb57 100644 --- a/data/web/inc/functions.quarantaine.inc.php +++ b/data/web/inc/functions.quarantine.inc.php @@ -1,5 +1,5 @@ 'danger', 'msg' => sprintf($lang['danger']['access_denied']) @@ -28,12 +28,12 @@ function quarantaine($_action, $_data = null) { return false; } try { - $stmt = $pdo->prepare('SELECT `rcpt` FROM `quarantaine` WHERE `id` = :id'); + $stmt = $pdo->prepare('SELECT `rcpt` FROM `quarantine` WHERE `id` = :id'); $stmt->execute(array(':id' => $id)); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) { try { - $stmt = $pdo->prepare("DELETE FROM `quarantaine` WHERE `id` = :id"); + $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id"); $stmt->execute(array( ':id' => $id )); @@ -67,7 +67,7 @@ function quarantaine($_action, $_data = null) { ); break; case 'edit': - if (!isset($_SESSION['acl']['quarantaine']) || $_SESSION['acl']['quarantaine'] != "1" ) { + if (!isset($_SESSION['acl']['quarantine']) || $_SESSION['acl']['quarantine'] != "1" ) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['access_denied']) @@ -121,7 +121,7 @@ function quarantaine($_action, $_data = null) { return false; } try { - $stmt = $pdo->prepare('SELECT `msg`, `qid`, `sender`, `rcpt` FROM `quarantaine` WHERE `id` = :id'); + $stmt = $pdo->prepare('SELECT `msg`, `qid`, `sender`, `rcpt` FROM `quarantine` WHERE `id` = :id'); $stmt->execute(array(':id' => $id)); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) { @@ -167,13 +167,13 @@ function quarantaine($_action, $_data = null) { $mail->Port = 590; $mail->setFrom($sender); $mail->CharSet = 'UTF-8'; - $mail->Subject = sprintf($lang['quarantaine']['release_subject'], $row['qid']); + $mail->Subject = sprintf($lang['quarantine']['release_subject'], $row['qid']); $mail->addAddress($row['rcpt']); $mail->IsHTML(false); $msg_tmpf = tempnam("/tmp", $row['qid']); file_put_contents($msg_tmpf, $row['msg']); $mail->addAttachment($msg_tmpf, $row['qid'] . '.eml'); - $mail->Body = sprintf($lang['quarantaine']['release_body']); + $mail->Body = sprintf($lang['quarantine']['release_body']); $mail->send(); unlink($msg_tmpf); } @@ -186,7 +186,7 @@ function quarantaine($_action, $_data = null) { return false; } try { - $stmt = $pdo->prepare("DELETE FROM `quarantaine` WHERE `id` = :id"); + $stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id"); $stmt->execute(array( ':id' => $id )); @@ -209,7 +209,7 @@ function quarantaine($_action, $_data = null) { case 'get': try { if ($_SESSION['mailcow_cc_role'] == "user") { - $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine` WHERE `rcpt` = :mbox'); + $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` = :mbox'); $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { @@ -217,7 +217,7 @@ function quarantaine($_action, $_data = null) { } } elseif ($_SESSION['mailcow_cc_role'] == "admin") { - $stmt = $pdo->query('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine`'); + $stmt = $pdo->query('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine`'); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { $q_meta[] = $row; @@ -226,7 +226,7 @@ function quarantaine($_action, $_data = null) { else { $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')); foreach ($domains as $domain) { - $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine` WHERE `rcpt` REGEXP :domain'); + $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` REGEXP :domain'); $stmt->execute(array(':domain' => '@' . $domain . '$')); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { @@ -270,7 +270,7 @@ function quarantaine($_action, $_data = null) { return false; } try { - $stmt = $pdo->prepare('SELECT `rcpt`, `symbols`, `msg`, `domain` FROM `quarantaine` WHERE `id`= :id'); + $stmt = $pdo->prepare('SELECT `rcpt`, `symbols`, `msg`, `domain` FROM `quarantine` WHERE `id`= :id'); $stmt->execute(array(':id' => $_data)); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) { @@ -287,4 +287,4 @@ function quarantaine($_action, $_data = null) { return false; break; } -} \ No newline at end of file +} diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index 369cc47a..3b464a49 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -1,9 +1,10 @@ - + + <?=$UI_TEXTS['title_name'];?>
diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php index fe7e19c5..2f4e010c 100644 --- a/data/web/modals/mailbox.php +++ b/data/web/modals/mailbox.php @@ -595,6 +595,45 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
+ +