diff --git a/.gitignore b/.gitignore index e535c710..0653ab8d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ data/conf/nginx/*.conf data/conf/nginx/*.custom data/conf/nginx/*.bak data/conf/dovecot/extra.conf +data/conf/rspamd/custom/* +data/conf/portainer/ +docker-compose.override.yml diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index c9a91dfa..37e150f6 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -37,7 +37,7 @@ mkdir -p ${ACME_BASE}/acme/private restart_containers(){ for container in $*; do log_f "Restarting ${container}..." no_nl - C_REST_OUT=$(curl -X POST http://dockerapi:8080/containers/${container}/restart | jq -r '.msg') + C_REST_OUT=$(curl -X POST --insecure https://dockerapi/containers/${container}/restart | jq -r '.msg') log_f "${C_REST_OUT}" no_date done } @@ -125,7 +125,7 @@ else fi log_f "Waiting for database... " -while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do +while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do sleep 2 done log_f "Initializing, please wait... " @@ -161,19 +161,19 @@ while true; do fi # Container ids may have changed - CONTAINERS_RESTART=($(curl --silent http://dockerapi:8080/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("nginx-mailcow") or contains("postfix-mailcow") or contains("dovecot-mailcow")) | .id' | tr "\n" " ")) + CONTAINERS_RESTART=($(curl --silent --insecure https://dockerapi/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("nginx-mailcow") or contains("postfix-mailcow") or contains("dovecot-mailcow")) | .id' | tr "\n" " ")) log_f "Waiting for domain table... " no_nl while [[ -z ${DOMAIN_TABLE} ]]; do curl --silent http://nginx/ >/dev/null 2>&1 - DOMAIN_TABLE=$(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) + DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) [[ -z ${DOMAIN_TABLE} ]] && sleep 10 done log_f "OK" no_date while read domains; do SQL_DOMAIN_ARR+=("${domains}") - done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 UNION SELECT alias_domain FROM alias_domain" -Bs) + done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 UNION SELECT alias_domain FROM alias_domain" -Bs) for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1) diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 78389c4f..9d0bdaec 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,8 +1,10 @@ -FROM python:2-alpine +FROM alpine:3.8 LABEL maintainer "Andre Peters " -RUN apk add -U --no-cache iptables ip6tables tzdata -RUN pip install docker==3.0.1 flask flask-restful +RUN apk add -U --no-cache python2 python-dev py-pip gcc musl-dev tzdata openssl-dev libffi-dev \ + && pip2 install --upgrade docker==3.0.1 flask flask-restful pyOpenSSL \ + && apk del python-dev py2-pip gcc COPY server.py / + CMD ["python2", "-u", "/server.py"] diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py index e66cf238..17e5e0fd 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -3,12 +3,16 @@ from flask_restful import Resource, Api from flask import jsonify from flask import request from threading import Thread +from OpenSSL import crypto import docker +import uuid import signal import time import os import re import sys +import ssl +import socket docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') app = Flask(__name__) @@ -93,22 +97,74 @@ class container_post(Resource): return sieve_return.output except Exception as e: return jsonify(type='danger', msg=str(e)) + # not in use... + elif request.json['cmd'] == 'mail_crypt_generate' and request.json['username'] and request.json['old_password'] and request.json['new_password']: + try: + for container in docker_client.containers.list(filters={"id": container_id}): + # create if missing + crypto_generate = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm mailbox cryptokey generate -u '" + request.json['username'].replace("'", "'\\''") + "' -URf"], user='vmail') + if crypto_generate.exit_code == 0: + # open a shell, bind stdin and return socket + cryptokey_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='vmail') + # command to be piped to shell + cryptokey_cmd = "/usr/local/bin/doveadm mailbox cryptokey password -u '" + request.json['username'].replace("'", "'\\''") + "' -n '" + request.json['new_password'].replace("'", "'\\''") + "' -o '" + request.json['old_password'].replace("'", "'\\''") + "'\n" + # socket is .output + cryptokey_socket = cryptokey_shell.output; + try : + # send command utf-8 encoded + cryptokey_socket.sendall(cryptokey_cmd.encode('utf-8')) + # we won't send more data than this + cryptokey_socket.shutdown(socket.SHUT_WR) + except socket.error: + # exit on socket error + return jsonify(type='danger', msg=str('socket error')) + # read response + cryptokey_response = recv_socket_data(cryptokey_socket) + crypto_error = re.search('dcrypt_key_load_private.+failed.+error', cryptokey_response) + if crypto_error is not None: + return jsonify(type='danger', msg=str("dcrypt_key_load_private error")) + return jsonify(type='success', msg=str("key pair generated")) + else: + return jsonify(type='danger', msg=str(crypto_generate.output)) + except Exception as e: + return jsonify(type='danger', msg=str(e)) + elif request.json['cmd'] == 'maildir_cleanup' and request.json['maildir']: + try: + for container in docker_client.containers.list(filters={"id": container_id}): + sane_name = re.sub(r'\W+', '', request.json['maildir']) + maildir_cleanup = container.exec_run(["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"], user='vmail') + if maildir_cleanup.exit_code == 0: + return jsonify(type='success', msg=str("moved to garbage")) + else: + return jsonify(type='danger', msg=str(maildir_cleanup.output)) + except Exception as 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') - if hash.exit_code == 0: - hash_stdout = str(hash.output) - for line in hash_stdout.split("\n"): - if '$2$' in line: - hash = line.strip() - f = open("/access.inc", "w") - f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash.rstrip()) + '";\n') - f.close() - container.restart() + worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd') + worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n" + worker_socket = worker_shell.output; + try : + worker_socket.sendall(worker_cmd.encode('utf-8')) + worker_socket.shutdown(socket.SHUT_WR) + except socket.error: + return jsonify(type='danger', msg=str('socket error')) + worker_response = recv_socket_data(worker_socket) + matched = False + for line in worker_response.split("\n"): + if '$2$' in line: + matched = True + hash = line.strip() + hash_out = re.search('\$2\$.+$', hash).group(0) + f = open("/access.inc", "w") + f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n') + f.close() + container.restart() + if matched: 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)) + return jsonify(type='danger', msg='command did not complete') except Exception as e: return jsonify(type='danger', msg=str(e)) elif request.json['cmd'] == 'mailman_password' and request.json['email'] and request.json['passwd']: @@ -137,11 +193,62 @@ class GracefulKiller: signal.signal(signal.SIGINT, self.exit_gracefully) signal.signal(signal.SIGTERM, self.exit_gracefully) - def exit_gracefully(self,signum, frame): + def exit_gracefully(self, signum, frame): self.kill_now = True def startFlaskAPI(): - app.run(debug=False, host='0.0.0.0', port=8080, threaded=True) + create_self_signed_cert() + try: + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.check_hostname = False + ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem') + except: + print "Cannot initialize TLS, retrying in 5s..." + time.sleep(5) + app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx) + +def recv_socket_data(c_socket, timeout=10): + c_socket.setblocking(0) + total_data=[]; + data=''; + begin=time.time() + while True: + if total_data and time.time()-begin > timeout: + break + elif time.time()-begin > timeout*2: + break + try: + data = c_socket.recv(8192) + if data: + total_data.append(data) + #change the beginning time for measurement + begin=time.time() + else: + #sleep for sometime to indicate a gap + time.sleep(0.1) + break + except: + pass + return ''.join(total_data) + +def create_self_signed_cert(): + pkey = crypto.PKey() + pkey.generate_key(crypto.TYPE_RSA, 2048) + cert = crypto.X509() + cert.get_subject().O = "mailcow" + cert.get_subject().CN = "dockerapi" + cert.set_serial_number(int(uuid.uuid4())) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(10*365*24*60*60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(pkey) + cert.sign(pkey, 'sha512') + cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) + with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle: + handle.write(cert) + with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle: + handle.write(pkey) api.add_resource(containers_get, '/containers/json') api.add_resource(container_get, '/containers//json') diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 03323914..fdfa84f5 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -14,6 +14,7 @@ RUN apt-get update && apt-get -y --no-install-recommends install \ cpanminus \ curl \ default-libmysqlclient-dev \ + dnsutils \ libjson-webtoken-perl \ libcgi-pm-perl \ libcrypt-openssl-rsa-perl \ @@ -88,10 +89,11 @@ RUN curl https://www.dovecot.org/releases/2.3/dovecot-$DOVECOT_VERSION.tar.gz | && rm -rf dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION RUN cpanm Data::Uniqid Mail::IMAPClient String::Util -RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync -RUN echo '30 3 * * * vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync -RUN echo '* * * * * root /usr/local/bin/trim_logs.sh >> /dev/stdout 2>&1' > /etc/cron.d/trim_logs - +RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync +RUN echo '30 3 * * * vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync +RUN echo '* * * * * vmail /usr/local/bin/trim_logs.sh >> /dev/stdout 2>&1' > /etc/cron.d/trim_logs +RUN echo '25 * * * * vmail /usr/local/bin/maildir_gc.sh >> /dev/stdout 2>&1' > /etc/cron.d/maildir_gc +RUN echo '30 1 * * * root /usr/local/bin/sa-rules.sh >> /dev/stdout 2>&1' > /etc/cron.d/sa-rules COPY trim_logs.sh /usr/local/bin/trim_logs.sh COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY imapsync /usr/local/bin/imapsync @@ -101,6 +103,8 @@ COPY report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve COPY report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve COPY rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham COPY rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam +COPY sa-rules.sh /usr/local/bin/sa-rules.sh +COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh COPY docker-entrypoint.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf @@ -109,7 +113,9 @@ RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ /usr/local/bin/imapsync_cron.pl \ /usr/local/bin/postlogin.sh \ /usr/local/bin/imapsync \ - /usr/local/bin/trim_logs.sh + /usr/local/bin/trim_logs.sh \ + /usr/local/bin/sa-rules.sh \ + /usr/local/bin/maildir_gc.sh RUN groupadd -g 5000 vmail \ && groupadd -g 401 dovecot \ diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 70ffb701..db7ecb8b 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -2,7 +2,7 @@ set -e # Wait for MySQL to warm-up -while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do +while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do echo "Waiting for database to come up..." sleep 2 done @@ -15,6 +15,7 @@ sed -i "s/LOG_LINES/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh # Create missing directories [[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ +[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo @@ -23,7 +24,7 @@ DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') # Create quota dict for Dovecot cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf -connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/quota/storage table = quota2 @@ -40,7 +41,7 @@ EOF # Create dict used for sieve pre and postfilters cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf -connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/sieve/name/\$script_name table = sieve_before @@ -62,7 +63,7 @@ map { EOF cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf -connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/sieve/name/\$script_name table = sieve_after @@ -87,7 +88,7 @@ EOF # Create userdb dict for Dovecot cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf driver = mysql -connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" 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 @@ -95,7 +96,7 @@ EOF # Create pass dict for Dovecot cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf driver = mysql -connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS} ssl_verify_server_cert=no ssl_ca=/etc/ssl/certs/ca-certificates.crt" 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') AND JSON_EXTRACT(attributes, '$.force_pw_update') NOT LIKE '%%1%%' EOF @@ -106,12 +107,14 @@ cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve # Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi +if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi # Create random master for SOGo sieve features RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) -echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd -echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds + +echo ${RAND_USER}@mailcow.local:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds # 401 is user dovecot if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then @@ -138,7 +141,10 @@ touch /etc/crontab /etc/cron.*/* # Clean stopped imapsync jobs rm -f /tmp/imapsync_busy.lock -IMAPSYNC_TABLE=$(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs) -[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'" +IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs) +[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'" + +# Collect SA rules once now +/usr/local/bin/sa-rules.sh exec "$@" diff --git a/data/Dockerfiles/dovecot/maildir_gc.sh b/data/Dockerfiles/dovecot/maildir_gc.sh new file mode 100755 index 00000000..24c1e461 --- /dev/null +++ b/data/Dockerfiles/dovecot/maildir_gc.sh @@ -0,0 +1,2 @@ +#/bin/bash +[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r {} \; diff --git a/data/Dockerfiles/dovecot/postlogin.sh b/data/Dockerfiles/dovecot/postlogin.sh index 343910ff..01a45f31 100755 --- a/data/Dockerfiles/dovecot/postlogin.sh +++ b/data/Dockerfiles/dovecot/postlogin.sh @@ -1,4 +1,3 @@ #!/bin/sh - export MASTER_USER=$USER exec "$@" diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham index 9d961be0..9b26817c 100755 --- a/data/Dockerfiles/dovecot/rspamd-pipe-ham +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -3,7 +3,7 @@ FILE=/tmp/mail$$ cat > $FILE trap "/bin/rm -f $FILE" 0 1 2 3 13 15 -cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnham -cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam index 3b9e3497..d06aa919 100755 --- a/data/Dockerfiles/dovecot/rspamd-pipe-spam +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -3,7 +3,7 @@ FILE=/tmp/mail$$ cat > $FILE trap "/bin/rm -f $FILE" 0 1 2 3 13 15 -cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnspam -cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd exit 0 diff --git a/data/Dockerfiles/dovecot/sa-rules.sh b/data/Dockerfiles/dovecot/sa-rules.sh new file mode 100755 index 00000000..0cea240c --- /dev/null +++ b/data/Dockerfiles/dovecot/sa-rules.sh @@ -0,0 +1,25 @@ +#!/bin/bash +[[ ! -d /tmp/sa-rules-heinlein ]] && mkdir -p /tmp/sa-rules-heinlein +if [[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]]; then + HASH_SA_RULES=0 +else + HASH_SA_RULES=$(cat /etc/rspamd/custom/sa-rules-heinlein | md5sum | cut -d' ' -f1) +fi + +curl --connect-timeout 15 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"').tar.gz --output /tmp/sa-rules.tar.gz +if [[ -f /tmp/sa-rules.tar.gz ]]; then + tar xfvz /tmp/sa-rules.tar.gz -C /tmp/sa-rules-heinlein + # create complete list of rules in a single file + cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules-heinlein + # Only restart rspamd-mailcow when rules changed + if [[ $(cat /etc/rspamd/custom/sa-rules-heinlein | md5sum | cut -d' ' -f1) != ${HASH_SA_RULES} ]]; then + CONTAINER_NAME=rspamd-mailcow + CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \ + jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | \ + jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | .id") + if [[ ! -z ${CONTAINER_ID} ]]; then + curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart + fi + fi +fi +rm -rf /tmp/sa-rules-heinlein /tmp/sa-rules.tar.gz diff --git a/data/Dockerfiles/dovecot/trim_logs.sh b/data/Dockerfiles/dovecot/trim_logs.sh index b8da9740..8489f27a 100755 --- a/data/Dockerfiles/dovecot/trim_logs.sh +++ b/data/Dockerfiles/dovecot/trim_logs.sh @@ -1,8 +1,7 @@ #!/bin/bash - -redis-cli -h redis LTRIM ACME_LOG 0 LOG_LINES -redis-cli -h redis LTRIM POSTFIX_MAILLOG 0 LOG_LINES -redis-cli -h redis LTRIM DOVECOT_MAILLOG 0 LOG_LINES -redis-cli -h redis LTRIM SOGO_LOG 0 LOG_LINES -redis-cli -h redis LTRIM NETFILTER_LOG 0 LOG_LINES -redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 LOG_LINES +/usr/bin/redis-cli -h redis LTRIM ACME_LOG 0 LOG_LINES +/usr/bin/redis-cli -h redis LTRIM POSTFIX_MAILLOG 0 LOG_LINES +/usr/bin/redis-cli -h redis LTRIM DOVECOT_MAILLOG 0 LOG_LINES +/usr/bin/redis-cli -h redis LTRIM SOGO_LOG 0 LOG_LINES +/usr/bin/redis-cli -h redis LTRIM NETFILTER_LOG 0 LOG_LINES +/usr/bin/redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 LOG_LINES diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 3acfd09f..0ed81896 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,11 +1,11 @@ FROM php:7.2-fpm-alpine3.7 LABEL maintainer "Andre Peters " -ENV APCU_PECL 5.1.11 +ENV APCU_PECL 5.1.12 ENV IMAGICK_PECL 3.4.3 ENV MAILPARSE_PECL 3.0.2 ENV MEMCACHED_PECL 3.0.4 -ENV REDIS_PECL 4.0.2 +ENV REDIS_PECL 4.1.1 RUN apk add -U --no-cache autoconf \ bash \ diff --git a/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/data/Dockerfiles/phpfpm/docker-entrypoint.sh index 44d46f74..50c5275d 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -4,11 +4,13 @@ set -e function array_by_comma { local IFS=","; echo "$*"; } # Wait for containers -while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do +while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for SQL..." sleep 2 done until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do + echo "Waiting for Redis..." sleep 2 done @@ -18,11 +20,11 @@ redis-cli -h redis-mailcow DEL DOMAIN_MAP while read line do DOMAIN_ARR+=("$line") -done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs) +done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs) while read line do DOMAIN_ARR+=("$line") -done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) +done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) if [[ ! -z ${DOMAIN_ARR} ]]; then for domain in "${DOMAIN_ARR[@]}"; do @@ -48,7 +50,7 @@ if [[ ${API_ALLOW_FROM} != "invalid" ]] && \ done VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]}) if [[ ! -z ${VALIDATED_IPS} ]]; then - mysql --host mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF INSERT INTO api (username, api_key, active, allow_from) SELECT username, "${API_KEY}", '1', "${VALIDATED_IPS}" FROM admin WHERE superadmin='1' AND active='1' ON DUPLICATE KEY UPDATE active = '1', allow_from = "${VALIDATED_IPS}", api_key = "${API_KEY}"; diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 28e51ccd..628322d8 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -14,7 +14,7 @@ newaliases; cat < /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT DISTINCT CASE WHEN '%d' IN ( @@ -29,10 +29,18 @@ query = SELECT DISTINCT END AS result; EOF +cat < /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_override WHERE active = '1' AND dest = '%s' +EOF + cat < /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT IF(EXISTS( SELECT 'TLS_ACTIVE' FROM alias @@ -49,7 +57,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps FROM ( @@ -80,7 +88,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts WHERE id IN ( @@ -96,7 +104,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT goto FROM alias, alias_domain WHERE alias_domain.alias_domain = '%d' @@ -107,7 +115,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT username FROM mailbox, alias_domain WHERE alias_domain.alias_domain = '%d' @@ -119,7 +127,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT goto FROM alias WHERE address='%s' @@ -129,7 +137,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT bcc_dest FROM bcc_maps WHERE local_dest='%s' @@ -140,7 +148,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT bcc_dest FROM bcc_maps WHERE local_dest='%s' @@ -151,7 +159,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT new_dest FROM recipient_maps WHERE old_dest='%s' @@ -161,7 +169,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1' UNION @@ -174,7 +182,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1' EOF @@ -182,7 +190,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1' EOF @@ -190,7 +198,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} # First select queries domain and alias_domain to determine if domains are active. query = SELECT goto FROM alias @@ -231,7 +239,7 @@ EOF cat < /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf user = ${DBUSER} password = ${DBPASS} -hosts = mysql +hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' diff --git a/data/Dockerfiles/postfix/rspamd-pipe-ham b/data/Dockerfiles/postfix/rspamd-pipe-ham index 9d961be0..9b26817c 100755 --- a/data/Dockerfiles/postfix/rspamd-pipe-ham +++ b/data/Dockerfiles/postfix/rspamd-pipe-ham @@ -3,7 +3,7 @@ FILE=/tmp/mail$$ cat > $FILE trap "/bin/rm -f $FILE" 0 1 2 3 13 15 -cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnham -cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd exit 0 diff --git a/data/Dockerfiles/postfix/rspamd-pipe-spam b/data/Dockerfiles/postfix/rspamd-pipe-spam index 3b9e3497..d06aa919 100755 --- a/data/Dockerfiles/postfix/rspamd-pipe-spam +++ b/data/Dockerfiles/postfix/rspamd-pipe-spam @@ -3,7 +3,7 @@ FILE=/tmp/mail$$ cat > $FILE trap "/bin/rm -f $FILE" 0 1 2 3 13 15 -cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/learnspam -cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/fuzzyadd +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd exit 0 diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 67b8aa1c..daf9760b 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update && apt-get install -y \ && mkdir -p /run/rspamd \ && chown _rspamd:_rspamd /run/rspamd -COPY settings.conf /etc/rspamd/modules.d/settings.conf +COPY settings.conf /etc/rspamd/settings.conf COPY docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index afb03bb6..6288550d 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -1,6 +1,9 @@ #!/bin/bash -chown -R _rspamd:_rspamd /var/lib/rspamd +chown -R _rspamd:_rspamd /var/lib/rspamd /etc/rspamd/local.d /etc/rspamd/override.d /etc/rspamd/custom +chmod 755 /var/lib/rspamd [[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Placeholder' > /etc/rspamd/override.d/worker-controller-password.inc +chown _rspamd:_rspamd /etc/rspamd/override.d/worker-controller-password.inc +[[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]] && echo '# to be auto-filled by dovecot-mailcow' > /etc/rspamd/custom/sa-rules-heinlein exec "$@" diff --git a/data/Dockerfiles/rspamd/lua_util.lua b/data/Dockerfiles/rspamd/lua_util.lua deleted file mode 100644 index a9abd901..00000000 --- a/data/Dockerfiles/rspamd/lua_util.lua +++ /dev/null @@ -1,152 +0,0 @@ -local exports = {} -local lpeg = require 'lpeg' - -local split_grammar = {} -local function rspamd_str_split(s, sep) - local gr = split_grammar[sep] - - if not gr then - local _sep = lpeg.P(sep) - local elem = lpeg.C((1 - _sep)^0) - local p = lpeg.Ct(elem * (_sep * elem)^0) - gr = p - split_grammar[sep] = gr - end - - return gr:match(s) -end - -exports.rspamd_str_split = rspamd_str_split - -local space = lpeg.S' \t\n\v\f\r' -local nospace = 1 - space -local ptrim = space^0 * lpeg.C((space^0 * nospace^1)^0) -local match = lpeg.match -exports.rspamd_str_trim = function(s) - return match(ptrim, s) -end - --- Robert Jay Gould http://lua-users.org/wiki/SimpleRound -exports.round = function(num, numDecimalPlaces) - local mult = 10^(numDecimalPlaces or 0) - return math.floor(num * mult) / mult -end - -exports.template = function(tmpl, keys) - local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" } - local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) } - local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") } - - local template_grammar = lpeg.Cs((var + var_braced + 1)^0) - - return lpeg.match(template_grammar, tmpl) -end - -exports.remove_email_aliases = function(email_addr) - local function check_gmail_user(addr) - -- Remove all points - local no_dots_user = string.gsub(addr.user, '%.', '') - local cap, pluses = string.match(no_dots_user, '^([^%+][^%+]*)(%+.*)$') - if cap then - return cap, rspamd_str_split(pluses, '+'), nil - elseif no_dots_user ~= addr.user then - return no_dots_user,{},nil - end - - return nil - end - - local function check_address(addr) - if addr.user then - local cap, pluses = string.match(addr.user, '^([^%+][^%+]*)(%+.*)$') - if cap then - return cap, rspamd_str_split(pluses, '+'), nil - end - end - - return nil - end - - local function set_addr(addr, new_user, new_domain) - if new_user then - addr.user = new_user - end - if new_domain then - addr.domain = new_domain - end - - if addr.domain then - addr.addr = string.format('%s@%s', addr.user, addr.domain) - else - addr.addr = string.format('%s@', addr.user) - end - - if addr.name and #addr.name > 0 then - addr.raw = string.format('"%s" <%s>', addr.name, addr.addr) - else - addr.raw = string.format('<%s>', addr.addr) - end - end - - local function check_gmail(addr) - local nu, tags, nd = check_gmail_user(addr) - - if nu then - return nu, tags, nd - end - - return nil - end - - local function check_googlemail(addr) - local nd = 'gmail.com' - local nu, tags = check_gmail_user(addr) - - if nu then - return nu, tags, nd - end - - return nil, nil, nd - end - - local specific_domains = { - ['gmail.com'] = check_gmail, - ['googlemail.com'] = check_googlemail, - } - - if email_addr then - if email_addr.domain and specific_domains[email_addr.domain] then - local nu, tags, nd = specific_domains[email_addr.domain](email_addr) - if nu or nd then - set_addr(email_addr, nu, nd) - - return nu, tags - end - else - local nu, tags, nd = check_address(email_addr) - if nu or nd then - set_addr(email_addr, nu, nd) - - return nu, tags - end - end - - return nil - end -end - -exports.is_rspamc_or_controller = function(task) - local ua = task:get_request_header('User-Agent') or '' - local pwd = task:get_request_header('Password') - local is_rspamc = false - if tostring(ua) == 'rspamc' or pwd then is_rspamc = true end - - return is_rspamc -end - -local unpack_function = table.unpack or unpack -exports.unpack = function(t) - return unpack_function(t) -end - -return exports diff --git a/data/Dockerfiles/rspamd/ratelimit.lua b/data/Dockerfiles/rspamd/ratelimit.lua deleted file mode 100644 index 839ec5c6..00000000 --- a/data/Dockerfiles/rspamd/ratelimit.lua +++ /dev/null @@ -1,674 +0,0 @@ ---[[ -Copyright (c) 2011-2017, Vsevolod Stakhov -Copyright (c) 2016-2017, Andrew Lewis - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -]]-- - -if confighelp then - return -end - --- A plugin that implements ratelimits using redis - -local E = {} -local N = 'ratelimit' -local redis_params --- Senders that are considered as bounce -local settings = { - bounce_senders = { 'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon' }, --- Do not check ratelimits for these recipients - whitelisted_rcpts = { 'postmaster', 'mailer-daemon' }, - prefix = 'RL', - ham_factor_rate = 1.01, - spam_factor_rate = 0.99, - ham_factor_burst = 1.02, - spam_factor_burst = 0.98, - max_rate_mult = 5, - max_bucket_mult = 10, - expire = 60 * 60 * 24 * 2, -- 2 days by default - limits = {}, - allow_local = false, -} - --- Checks bucket, updating it if needed --- KEYS[1] - prefix to update, e.g. RL__ --- KEYS[2] - current time in milliseconds --- KEYS[3] - bucket leak rate (messages per millisecond) --- KEYS[4] - bucket burst --- KEYS[5] - expire for a bucket --- return 1 if message should be ratelimited and 0 if not --- Redis keys used: --- l - last hit --- b - current burst --- dr - current dynamic rate multiplier (*10000) --- db - current dynamic burst multiplier (*10000) -local bucket_check_script = [[ - local last = redis.call('HGET', KEYS[1], 'l') - local now = tonumber(KEYS[2]) - local dynr, dynb = 0, 0 - if not last then - -- New bucket - redis.call('HSET', KEYS[1], 'l', KEYS[2]) - redis.call('HSET', KEYS[1], 'b', '0') - redis.call('HSET', KEYS[1], 'dr', '10000') - redis.call('HSET', KEYS[1], 'db', '10000') - redis.call('EXPIRE', KEYS[1], KEYS[5]) - return {0, 0, 1, 1} - end - - last = tonumber(last) - local burst = tonumber(redis.call('HGET', KEYS[1], 'b')) - -- Perform leak - if burst > 0 then - if last < tonumber(KEYS[2]) then - local rate = tonumber(KEYS[3]) - dynr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000.0 - rate = rate * dynr - local leaked = ((now - last) * rate) - burst = burst - leaked - redis.call('HINCRBYFLOAT', KEYS[1], 'b', -(leaked)) - end - else - burst = 0 - redis.call('HSET', KEYS[1], 'b', '0') - end - - dynb = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000.0 - - if (burst + 1) * dynb > tonumber(KEYS[4]) then - return {1, tostring(burst), tostring(dynr), tostring(dynb)} - end - - return {0, tostring(burst), tostring(dynr), tostring(dynb)} -]] -local bucket_check_id - - --- Updates a bucket --- KEYS[1] - prefix to update, e.g. RL__ --- KEYS[2] - current time in milliseconds --- KEYS[3] - dynamic rate multiplier --- KEYS[4] - dynamic burst multiplier --- KEYS[5] - max dyn rate (min: 1/x) --- KEYS[6] - max burst rate (min: 1/x) --- KEYS[7] - expire for a bucket --- Redis keys used: --- l - last hit --- b - current burst --- dr - current dynamic rate multiplier --- db - current dynamic burst multiplier -local bucket_update_script = [[ - local last = redis.call('HGET', KEYS[1], 'l') - local now = tonumber(KEYS[2]) - if not last then - -- New bucket - redis.call('HSET', KEYS[1], 'l', KEYS[2]) - redis.call('HSET', KEYS[1], 'b', '1') - redis.call('HSET', KEYS[1], 'dr', '10000') - redis.call('HSET', KEYS[1], 'db', '10000') - redis.call('EXPIRE', KEYS[1], KEYS[7]) - return {1, 1, 1} - end - - local burst = tonumber(redis.call('HGET', KEYS[1], 'b')) - local db = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000 - local dr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000 - - if dr < tonumber(KEYS[5]) and dr > 1.0 / tonumber(KEYS[5]) then - dr = dr * tonumber(KEYS[3]) - redis.call('HSET', KEYS[1], 'dr', tostring(math.floor(dr * 10000))) - end - - if db < tonumber(KEYS[6]) and db > 1.0 / tonumber(KEYS[6]) then - db = db * tonumber(KEYS[4]) - redis.call('HSET', KEYS[1], 'db', tostring(math.floor(db * 10000))) - end - - redis.call('HINCRBYFLOAT', KEYS[1], 'b', 1) - redis.call('HSET', KEYS[1], 'l', KEYS[2]) - redis.call('EXPIRE', KEYS[1], KEYS[7]) - - return {tostring(burst), tostring(dr), tostring(db)} -]] -local bucket_update_id - --- message_func(task, limit_type, prefix, bucket) -local message_func = function(_, limit_type, _, _) - return string.format('Ratelimit "%s" exceeded', limit_type) -end - -local rspamd_logger = require "rspamd_logger" -local rspamd_util = require "rspamd_util" -local rspamd_lua_utils = require "lua_util" -local lua_redis = require "lua_redis" -local fun = require "fun" -local lua_maps = require "lua_maps" -local lua_util = require "lua_util" -local rspamd_hash = require "rspamd_cryptobox_hash" - - -local function load_scripts(cfg, ev_base) - bucket_check_id = lua_redis.add_redis_script(bucket_check_script, redis_params) - bucket_update_id = lua_redis.add_redis_script(bucket_update_script, redis_params) -end - -local limit_parser -local function parse_string_limit(lim, no_error) - local function parse_time_suffix(s) - if s == 's' then - return 1 - elseif s == 'm' then - return 60 - elseif s == 'h' then - return 3600 - elseif s == 'd' then - return 86400 - end - end - local function parse_num_suffix(s) - if s == '' then - return 1 - elseif s == 'k' then - return 1000 - elseif s == 'm' then - return 1000000 - elseif s == 'g' then - return 1000000000 - end - end - local lpeg = require "lpeg" - - if not limit_parser then - local digit = lpeg.R("09") - limit_parser = {} - limit_parser.integer = - (lpeg.S("+-") ^ -1) * - (digit ^ 1) - limit_parser.fractional = - (lpeg.P(".") ) * - (digit ^ 1) - limit_parser.number = - (limit_parser.integer * - (limit_parser.fractional ^ -1)) + - (lpeg.S("+-") * limit_parser.fractional) - limit_parser.time = lpeg.Cf(lpeg.Cc(1) * - (limit_parser.number / tonumber) * - ((lpeg.S("smhd") / parse_time_suffix) ^ -1), - function (acc, val) return acc * val end) - limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) * - (limit_parser.number / tonumber) * - ((lpeg.S("kmg") / parse_num_suffix) ^ -1), - function (acc, val) return acc * val end) - limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number * - (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) * - limit_parser.time) - end - local t = lpeg.match(limit_parser.limit, lim) - - if t and t[1] and t[2] and t[2] ~= 0 then - return t[2], t[1] - end - - if not no_error then - rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim) - end - - return nil -end - -local function parse_limit(name, data) - local buckets = {} - if type(data) == 'table' then - -- 3 cases here: - -- * old limit in format [burst, rate] - -- * vector of strings in Andrew's string format - -- * proper bucket table - if #data == 2 and tonumber(data[1]) and tonumber(data[2]) then - -- Old style ratelimit - rspamd_logger.warnx(rspamd_config, 'old style ratelimit for %s', name) - if tonumber(data[1]) > 0 and tonumber(data[2]) > 0 then - table.insert(buckets, { - burst = data[1], - rate = data[2] - }) - elseif data[1] ~= 0 then - rspamd_logger.warnx(rspamd_config, 'invalid numbers for %s', name) - else - rspamd_logger.infox(rspamd_config, 'disable limit %s, burst is zero', name) - end - else - -- Recursively map parse_limit and flatten the list - fun.each(function(l) - -- Flatten list - for _,b in ipairs(l) do table.insert(buckets, b) end - end, fun.map(function(d) return parse_limit(d, name) end, data)) - end - elseif type(data) == 'string' then - local rep_rate, burst = parse_string_limit(data) - - if rep_rate and burst then - table.insert(buckets, { - burst = burst, - rate = 1.0 / rep_rate -- reciprocal - }) - end - end - - -- Filter valid - return fun.totable(fun.filter(function(val) - return type(val.burst) == 'number' and type(val.rate) == 'number' - end, buckets)) -end - ---- Check whether this addr is bounce -local function check_bounce(from) - return fun.any(function(b) return b == from end, settings.bounce_senders) -end - -local keywords = { - ['ip'] = { - ['get_value'] = function(task) - local ip = task:get_ip() - if ip and ip:is_valid() then return tostring(ip) end - return nil - end, - }, - ['rip'] = { - ['get_value'] = function(task) - local ip = task:get_ip() - if ip and ip:is_valid() and not ip:is_local() then return tostring(ip) end - return nil - end, - }, - ['from'] = { - ['get_value'] = function(task) - local from = task:get_from(0) - if ((from or E)[1] or E).addr then - return string.lower(from[1]['addr']) - end - return nil - end, - }, - ['bounce'] = { - ['get_value'] = function(task) - local from = task:get_from(0) - if not ((from or E)[1] or E).user then - return '_' - end - if check_bounce(from[1]['user']) then return '_' else return nil end - end, - }, - ['asn'] = { - ['get_value'] = function(task) - local asn = task:get_mempool():get_variable('asn') - if not asn then - return nil - else - return asn - end - end, - }, - ['user'] = { - ['get_value'] = function(task) - local auser = task:get_user() - if not auser then - return nil - else - return auser - end - end, - }, - ['to'] = { - ['get_value'] = function(task) - return task:get_principal_recipient() - end, - }, -} - -local function gen_rate_key(task, rtype, bucket) - local key_t = {tostring(lua_util.round(100000.0 / bucket.burst))} - local key_keywords = lua_util.str_split(rtype, '_') - local have_user = false - - for _, v in ipairs(key_keywords) do - local ret - - if keywords[v] and type(keywords[v]['get_value']) == 'function' then - ret = keywords[v]['get_value'](task) - end - if not ret then return nil end - if v == 'user' then have_user = true end - if type(ret) ~= 'string' then ret = tostring(ret) end - table.insert(key_t, ret) - end - - if have_user and not task:get_user() then - return nil - end - - return table.concat(key_t, ":") -end - -local function make_prefix(redis_key, name, bucket) - local hash_len = 24 - if hash_len > #redis_key then hash_len = #redis_key end - local hash = settings.prefix .. - string.sub(rspamd_hash.create(redis_key):base32(), 1, hash_len) - -- Fill defaults - if not bucket.spam_factor_rate then - bucket.spam_factor_rate = settings.spam_factor_rate - end - if not bucket.ham_factor_rate then - bucket.ham_factor_rate = settings.ham_factor_rate - end - if not bucket.spam_factor_burst then - bucket.spam_factor_burst = settings.spam_factor_burst - end - if not bucket.ham_factor_burst then - bucket.ham_factor_burst = settings.ham_factor_burst - end - - return { - bucket = bucket, - name = name, - hash = hash - } -end - -local function limit_to_prefixes(task, k, v, prefixes) - local n = 0 - for _,bucket in ipairs(v) do - local prefix = gen_rate_key(task, k, bucket) - - if prefix then - prefixes[prefix] = make_prefix(prefix, k, bucket) - n = n + 1 - end - end - - return n -end - -local function ratelimit_cb(task) - if not settings.allow_local and - rspamd_lua_utils.is_rspamc_or_controller(task) then return end - - -- Get initial task data - local ip = task:get_from_ip() - if ip and ip:is_valid() and settings.whitelisted_ip then - if settings.whitelisted_ip:get_key(ip) then - -- Do not check whitelisted ip - rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP') - return - end - end - -- Parse all rcpts - local rcpts = task:get_recipients() - local rcpts_user = {} - if rcpts then - fun.each(function(r) - fun.each(function(type) table.insert(rcpts_user, r[type]) end, {'user', 'addr'}) - end, rcpts) - - if fun.any(function(r) return settings.whitelisted_rcpts:get_key(r) end, rcpts_user) then - rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient') - return - end - end - -- Get user (authuser) - if settings.whitelisted_user then - local auser = task:get_user() - if settings.whitelisted_user:get_key(auser) then - rspamd_logger.infox(task, 'skip ratelimit for whitelisted user') - return - end - end - -- Now create all ratelimit prefixes - local prefixes = {} - local nprefixes = 0 - - for k,v in pairs(settings.limits) do - nprefixes = nprefixes + limit_to_prefixes(task, k, v, prefixes) - end - - for k, hdl in pairs(settings.custom_keywords or E) do - local ret, redis_key, bd = pcall(hdl, task) - - if ret then - local bucket = parse_limit(k, bd) - if bucket[1] then - prefixes[redis_key] = make_prefix(redis_key, k, bucket[1]) - end - nprefixes = nprefixes + 1 - else - rspamd_logger.errx(task, 'cannot call handler for %s: %s', - k, redis_key) - end - end - - local function gen_check_cb(prefix, bucket, lim_name) - return function(err, data) - if err then - rspamd_logger.errx('cannot check limit %s: %s %s', prefix, err, data) - elseif type(data) == 'table' and data[1] and data[1] == 1 then - -- set symbol only and do NOT soft reject - if settings.symbol then - task:insert_result(settings.symbol, 0.0, lim_name .. "(" .. prefix .. ")") - rspamd_logger.infox(task, - 'set_symbol_only: ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)', - lim_name, prefix, - bucket.burst, bucket.rate, - data[2], data[3], data[4]) - return - -- set INFO symbol and soft reject - elseif settings.info_symbol then - task:insert_result(settings.info_symbol, 1.0, - lim_name .. "(" .. prefix .. ")") - end - rspamd_logger.infox(task, - 'ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)', - lim_name, prefix, - bucket.burst, bucket.rate, - data[2], data[3], data[4]) - task:set_pre_result('soft reject', - message_func(task, lim_name, prefix, bucket)) - end - end - end - - -- Don't do anything if pre-result has been already set - if task:has_pre_result() then return end - - if nprefixes > 0 then - -- Save prefixes to the cache to allow update - task:cache_set('ratelimit_prefixes', prefixes) - local now = rspamd_util.get_time() - now = lua_util.round(now * 1000.0) -- Get milliseconds - -- Now call check script for all defined prefixes - - for pr,value in pairs(prefixes) do - local bucket = value.bucket - local rate = (bucket.rate) / 1000.0 -- Leak rate in messages/ms - rspamd_logger.debugm(N, task, "check limit %s:%s -> %s (%s/%s)", - value.name, pr, value.hash, bucket.burst, bucket.rate) - lua_redis.exec_redis_script(bucket_check_id, - {key = value.hash, task = task, is_write = true}, - gen_check_cb(pr, bucket, value.name), - {value.hash, tostring(now), tostring(rate), tostring(bucket.burst), - tostring(settings.expire)}) - end - end -end - -local function ratelimit_update_cb(task) - local prefixes = task:cache_get('ratelimit_prefixes') - - if prefixes then - if task:has_pre_result() then - -- Already rate limited/greylisted, do nothing - rspamd_logger.debugm(N, task, 'pre-action has been set, do not update') - return - end - - local is_spam = not (task:get_metric_action() == 'no action') - - -- Update each bucket - for k, v in pairs(prefixes) do - local bucket = v.bucket - local function update_bucket_cb(err, data) - if err then - rspamd_logger.errx(task, 'cannot update rate bucket %s: %s', - k, err) - else - rspamd_logger.debugm(N, task, - "updated limit %s:%s -> %s (%s/%s), burst: %s, dyn_rate: %s, dyn_burst: %s", - v.name, k, v.hash, - bucket.burst, bucket.rate, - data[1], data[2], data[3]) - end - end - local now = rspamd_util.get_time() - now = lua_util.round(now * 1000.0) -- Get milliseconds - local mult_burst = bucket.ham_factor_burst or 1.0 - local mult_rate = bucket.ham_factor_burst or 1.0 - - if is_spam then - mult_burst = bucket.spam_factor_burst or 1.0 - mult_rate = bucket.spam_factor_rate or 1.0 - end - - lua_redis.exec_redis_script(bucket_update_id, - {key = v.hash, task = task, is_write = true}, - update_bucket_cb, - {v.hash, tostring(now), tostring(mult_rate), tostring(mult_burst), - tostring(settings.max_rate_mult), tostring(settings.max_bucket_mult), - tostring(settings.expire)}) - end - end -end - -local opts = rspamd_config:get_all_opt(N) -if opts then - - settings = lua_util.override_defaults(settings, opts) - - if opts['limit'] then - rspamd_logger.errx(rspamd_config, 'Legacy ratelimit config format no longer supported') - end - - if opts['rates'] and type(opts['rates']) == 'table' then - -- new way of setting limits - fun.each(function(t, lim) - local buckets = parse_limit(t, lim) - - if buckets and #buckets > 0 then - settings.limits[t] = buckets - end - end, opts['rates']) - end - - local enabled_limits = fun.totable(fun.map(function(t) - return t - end, settings.limits)) - rspamd_logger.infox(rspamd_config, - 'enabled rate buckets: [%1]', table.concat(enabled_limits, ',')) - - -- Ret, ret, ret: stupid legacy stuff: - -- If we have a string with commas then load it as as static map - -- otherwise, apply normal logic of Rspamd maps - - local wrcpts = opts['whitelisted_rcpts'] - if type(wrcpts) == 'string' then - if string.find(wrcpts, ',') then - settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl( - lua_util.rspamd_str_split(wrcpts, ','), 'set', 'Ratelimit whitelisted rcpts') - else - settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set', - 'Ratelimit whitelisted rcpts') - end - elseif type(opts['whitelisted_rcpts']) == 'table' then - settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set', - 'Ratelimit whitelisted rcpts') - else - -- Stupid default... - settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl( - settings.whitelisted_rcpts, 'set', 'Ratelimit whitelisted rcpts') - end - - if opts['whitelisted_ip'] then - settings.whitelisted_ip = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix', - 'Ratelimit whitelist ip map') - end - - if opts['whitelisted_user'] then - settings.whitelisted_user = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_user', 'set', - 'Ratelimit whitelist user map') - end - - settings.custom_keywords = {} - if opts['custom_keywords'] then - local ret, res_or_err = pcall(loadfile(opts['custom_keywords'])) - - if ret then - opts['custom_keywords'] = {} - if type(res_or_err) == 'table' then - for k,hdl in pairs(res_or_err) do - settings['custom_keywords'][k] = hdl - end - elseif type(res_or_err) == 'function' then - settings['custom_keywords']['custom'] = res_or_err - end - else - rspamd_logger.errx(rspamd_config, 'cannot execute %s: %s', - opts['custom_keywords'], res_or_err) - settings['custom_keywords'] = {} - end - end - - if opts['message_func'] then - message_func = assert(load(opts['message_func']))() - end - - redis_params = lua_redis.parse_redis_server('ratelimit') - - if not redis_params then - rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module') - lua_util.disable_module(N, "redis") - else - local s = { - type = 'prefilter,nostat', - name = 'RATELIMIT_CHECK', - priority = 7, - callback = ratelimit_cb, - flags = 'empty', - } - - if settings.symbol then - s.name = settings.symbol - elseif settings.info_symbol then - s.name = settings.info_symbol - end - - rspamd_config:register_symbol(s) - rspamd_config:register_symbol { - type = 'idempotent', - name = 'RATELIMIT_UPDATE', - callback = ratelimit_update_cb, - } - end -end - -rspamd_config:add_on_load(function(cfg, ev_base, worker) - load_scripts(cfg, ev_base) -end) diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 30a06d24..9d3965a7 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -49,4 +49,6 @@ COPY sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg COPY acl.diff /acl.diff CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf +VOLUME /usr/lib/GNUstep/SOGo/ + RUN rm -rf /tmp/* /var/tmp/* diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh index 46d8ec6c..77f03b63 100755 --- a/data/Dockerfiles/sogo/bootstrap-sogo.sh +++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -1,7 +1,7 @@ #!/bin/bash # Wait for MySQL to warm-up -while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do +while ! mysqladmin ping --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do echo "Waiting for database to come up..." sleep 2 done @@ -15,10 +15,10 @@ done # Recreate view -mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view" +mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view" while [[ ${VIEW_OK} != 'OK' ]]; do - mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF + mysql --socket=/var/run/mysqld/mysqld.sock -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, 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, '($|,)') @@ -26,7 +26,7 @@ LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.usern WHERE mailbox.active = '1' GROUP BY mailbox.username; EOF - if [[ ! -z $(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then + if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then VIEW_OK=OK else echo "Will retry to setup SOGo view in 3s" @@ -37,11 +37,11 @@ done # Wait for static view table if missing after update and update content while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do - if [[ ! -z $(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then + if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then STATIC_VIEW_OK=OK echo "Updating _sogo_static_view content..." - mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view SELECT * from sogo_view" - mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')" + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view SELECT * from sogo_view" + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')" else echo "Waiting for database initialization..." sleep 3 @@ -50,10 +50,10 @@ done # Recreate password update trigger -mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password" +mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password" while [[ ${TRIGGER_OK} != 'OK' ]]; do - mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF DELIMITER - CREATE TRIGGER sogo_update_password AFTER UPDATE ON _sogo_static_view FOR EACH ROW @@ -63,7 +63,7 @@ END; - DELIMITER ; EOF - if [[ ! -z $(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then + if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then TRIGGER_OK=OK else echo "Will retry to setup SOGo password update trigger in 3s" @@ -81,19 +81,19 @@ cat < /var/lib/sogo/GNUstep/Defaults/sogod.plist OCSAclURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_acl + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl OCSCacheFolderURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_cache_folder + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder OCSEMailAlarmsFolderURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_alarms_folder + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_alarms_folder OCSFolderInfoURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_folder_info + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_folder_info OCSSessionsFolderURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_sessions_folder + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_sessions_folder OCSStoreURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_store + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_store SOGoProfileURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_user_profile + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_user_profile SOGoTimeZone ${TZ} domains @@ -138,11 +138,11 @@ while read line prependPasswordScheme YES viewURL - mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/_sogo_static_view + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/_sogo_static_view " >> /var/lib/sogo/GNUstep/Defaults/sogod.plist -done < <(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain;" -B -N) +done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain;" -B -N) # Generate footer echo ' diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index 2a889560..1c1422b3 100644 --- a/data/Dockerfiles/sogo/supervisord.conf +++ b/data/Dockerfiles/sogo/supervisord.conf @@ -16,13 +16,6 @@ command=/usr/sbin/cron -f autorestart=true priority=2 -[program:sogo-webres] -command=/usr/bin/python -u -m SimpleHTTPServer 9192 -directory=/usr/lib/GNUstep/SOGo/ -user=sogo -autorestart=true -priority=4 - [program:bootstrap-sogo] command=/bootstrap-sogo.sh stdout_logfile=/dev/stdout diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 72e86bc0..4f443b88 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -10,6 +10,7 @@ RUN apk add --update --no-cache \ drill \ && curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \ && chown root:unbound /etc/unbound \ + && adduser unbound tty \ && chmod 775 /etc/unbound EXPOSE 53/udp 53/tcp diff --git a/data/Dockerfiles/unbound/docker-entrypoint.sh b/data/Dockerfiles/unbound/docker-entrypoint.sh index b458cd8a..d179eaca 100755 --- a/data/Dockerfiles/unbound/docker-entrypoint.sh +++ b/data/Dockerfiles/unbound/docker-entrypoint.sh @@ -1,8 +1,11 @@ #!/bin/bash +echo "Setting console permissions..." +chown root:tty /dev/console +chmod g+rw /dev/console echo "Receiving anchor key..." /usr/sbin/unbound-anchor -a /etc/unbound/trusted-key.key echo "Receiving root hints..." curl -#o /etc/unbound/root.hints https://www.internic.net/domain/named.cache - +/usr/sbin/unbound-control-setup exec "$@" diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index c06abbc2..ab528a78 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -59,28 +59,34 @@ function mail_error() { log_msg "Sent notification email to ${1}" } - get_container_ip() { # ${1} is container CONTAINER_ID=() + CONTAINER_IPS=() CONTAINER_IP= LOOP_C=1 until [[ ${CONTAINER_IP} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || [[ ${LOOP_C} -gt 5 ]]; do sleep 0.5 # get long container id for exact match - CONTAINER_ID=($(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | .id")) + CONTAINER_ID=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | .id")) # returned id can have multiple elements (if scaled), shuffle for random test CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf)) if [[ ! -z ${CONTAINER_ID} ]]; then for matched_container in "${CONTAINER_ID[@]}"; do - CONTAINER_IP=$(curl --silent http://dockerapi:8080/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress') - # grep will do nothing if one of these vars is empty - [[ -z ${CONTAINER_IP} ]] && continue - [[ -z ${IPV4_NETWORK} ]] && continue - # only return ips that are part of our network - if ! grep -q ${IPV4_NETWORK} <(echo ${CONTAINER_IP}); then - CONTAINER_IP= - fi + CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) + for ip_match in "${CONTAINER_IPS[@]}"; do + # grep will do nothing if one of these vars is empty + [[ -z ${ip_match} ]] && continue + [[ -z ${IPV4_NETWORK} ]] && continue + # only return ips that are part of our network + if ! grep -q ${IPV4_NETWORK} <(echo ${ip_match}); then + continue + else + CONTAINER_IP=${ip_match} + break + fi + done + [[ ! -z ${CONTAINER_IP} ]] && break done fi LOOP_C=$((LOOP_C + 1)) @@ -88,7 +94,6 @@ get_container_ip() { [[ ${LOOP_C} -gt 5 ]] && echo 240.0.0.0 || echo ${CONTAINER_IP} } -# Check functions nginx_checks() { err_count=0 diff_c=0 @@ -118,8 +123,8 @@ mysql_checks() { while [ ${err_count} -lt ${THRESHOLD} ]; do host_ip=$(get_container_ip mysql-mailcow) err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_mysql -H ${host_ip} -P 3306 -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 1>&2; err_count=$(( ${err_count} + $? )) - /usr/lib/nagios/plugins/check_mysql_query -H ${host_ip} -P 3306 -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 1>&2; err_count=$(( ${err_count} + $? )) [ ${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 "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} @@ -138,7 +143,6 @@ sogo_checks() { while [ ${err_count} -lt ${THRESHOLD} ]; do host_ip=$(get_container_ip sogo-mailcow) err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /WebServerResources/css/theme-default.css -p 9192 -R md-default-theme 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 1>&2; err_count=$(( ${err_count} + $? )) [ ${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} )) @@ -222,7 +226,7 @@ rspamd_checks() { while [ ${err_count} -lt ${THRESHOLD} ]; do host_ip=$(get_container_ip rspamd-mailcow) err_c_cur=${err_count} - SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /rspamd-sock/rspamd.sock http://rspamd/scan -d ' + SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan -d ' To: null@localhost From: watchdog@localhost @@ -338,12 +342,12 @@ done # Monitor dockerapi ( while true; do - while nc -z dockerapi 8080; do + while nc -z dockerapi 443; do sleep 3 done log_msg "Cannot find dockerapi-mailcow, waiting to recover..." kill -STOP ${BACKGROUND_TASKS[*]} - until nc -z dockerapi 8080; do + until nc -z dockerapi 443; do sleep 3 done kill -CONT ${BACKGROUND_TASKS[*]} @@ -358,10 +362,10 @@ while true; do if [[ ${com_pipe_answer} =~ .+-mailcow ]]; then kill -STOP ${BACKGROUND_TASKS[*]} sleep 3 - CONTAINER_ID=$(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id") + CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id") if [[ ! -z ${CONTAINER_ID} ]]; then log_msg "Sending restart command to ${CONTAINER_ID}..." - curl --silent -XPOST http://dockerapi:8080/containers/${CONTAINER_ID}/restart + curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart fi log_msg "Wait for restarted container to settle and continue watching..." sleep 30s diff --git a/data/assets/mysql/docker-entrypoint.sh b/data/assets/mysql/docker-entrypoint.sh new file mode 100755 index 00000000..94e394ac --- /dev/null +++ b/data/assets/mysql/docker-entrypoint.sh @@ -0,0 +1,192 @@ +#!/bin/bash +set -eo pipefail +shopt -s nullglob + +openssl req -x509 -sha256 -newkey rsa:2048 -keyout /var/lib/mysql/sql.key -out /var/lib/mysql/sql.crt -days 3650 -nodes -subj '/CN=mysql' + +# if command starts with an option, prepend mysqld +if [ "${1:0:1}" = '-' ]; then + set -- mysqld "$@" +fi + +# skip setup if they want an option that stops mysqld +wantHelp= +for arg; do + case "$arg" in + -'?'|--help|--print-defaults|-V|--version) + wantHelp=1 + break + ;; + esac +done + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +_check_config() { + toRun=( "$@" --verbose --help --log-bin-index="$(mktemp -u)" ) + if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then + cat >&2 <<-EOM + + ERROR: mysqld failed while attempting to check config + command was: "${toRun[*]}" + + $errors + EOM + exit 1 + fi +} + +# Fetch value from server config +# We use mysqld --verbose --help instead of my_print_defaults because the +# latter only show values present in config files, and not server defaults +_get_config() { + local conf="$1"; shift + "$@" --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null | awk '$1 == "'"$conf"'" { print $2; exit }' +} + +# allow the container to be started with `--user` +if [ "$1" = 'mysqld' -a -z "$wantHelp" -a "$(id -u)" = '0' ]; then + _check_config "$@" + DATADIR="$(_get_config 'datadir' "$@")" + mkdir -p "$DATADIR" + chown -R mysql:mysql "$DATADIR" + exec gosu mysql "$BASH_SOURCE" "$@" +fi + +if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then + # still need to check config, container may have started with --user + _check_config "$@" + # Get config + DATADIR="$(_get_config 'datadir' "$@")" + + if [ ! -d "$DATADIR/mysql" ]; then + file_env 'MYSQL_ROOT_PASSWORD' + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then + echo >&2 'error: database is uninitialized and password option is not specified ' + echo >&2 ' You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD' + exit 1 + fi + + mkdir -p "$DATADIR" + + echo 'Initializing database' + # "Other options are passed to mysqld." (so we pass all "mysqld" arguments directly here) + mysql_install_db --datadir="$DATADIR" --rpm "${@:2}" + echo 'Database initialized' + + SOCKET="$(_get_config 'socket' "$@")" + "$@" --skip-networking --socket="${SOCKET}" & + pid="$!" + + mysql=( mysql --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" ) + + for i in {30..0}; do + if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then + break + fi + echo 'MySQL init process in progress...' + sleep 1 + done + if [ "$i" = 0 ]; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then + # sed is for https://bugs.mysql.com/bug.php?id=20545 + mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql + fi + + if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then + export MYSQL_ROOT_PASSWORD="$(pwgen -1 32)" + echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD" + fi + + rootCreate= + # default root to listen for connections from anywhere + file_env 'MYSQL_ROOT_HOST' '%' + if [ ! -z "$MYSQL_ROOT_HOST" -a "$MYSQL_ROOT_HOST" != 'localhost' ]; then + # no, we don't care if read finds a terminating character in this heredoc + # https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151 + read -r -d '' rootCreate <<-EOSQL || true + CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ; + EOSQL + fi + + "${mysql[@]}" <<-EOSQL + -- What's done in this file shouldn't be replicated + -- or products like mysql-fabric won't work + SET @@SESSION.SQL_LOG_BIN=0; + + DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ; + SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ; + GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; + ${rootCreate} + DROP DATABASE IF EXISTS test ; + FLUSH PRIVILEGES ; + EOSQL + + if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then + mysql+=( -p"${MYSQL_ROOT_PASSWORD}" ) + fi + + file_env 'MYSQL_DATABASE' + if [ "$MYSQL_DATABASE" ]; then + echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}" + mysql+=( "$MYSQL_DATABASE" ) + fi + + file_env 'MYSQL_USER' + file_env 'MYSQL_PASSWORD' + if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then + echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}" + + if [ "$MYSQL_DATABASE" ]; then + echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}" + fi + fi + + echo + for f in /docker-entrypoint-initdb.d/*; do + case "$f" in + *.sh) echo "$0: running $f"; . "$f" ;; + *.sql) echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;; + *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;; + *) echo "$0: ignoring $f" ;; + esac + echo + done + + if ! kill -s TERM "$pid" || ! wait "$pid"; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + echo + echo 'MySQL init process done. Ready for start up.' + echo + fi +fi + +exec "$@" diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index dd8db8a3..2df625e2 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -14,7 +14,7 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib listescape #mail_crypt +mail_plugins = quota acl zlib listescape mail_crypt mail_crypt_acl # Dovecot 2.2 #ssl_protocols = !SSLv3 @@ -175,7 +175,7 @@ namespace { type = shared separator = / prefix = Shared/%%u/ - location = maildir:%%h/:CONTROL=~/Shared/%%u:INDEXPVT=~/Shared/%%u + location = maildir:%%h/:INDEX=~/Shared/%%u;CONTROL=~/Shared/%%u subscriptions = no list = children } @@ -223,7 +223,7 @@ service pop3-login { } service imap { executable = imap imap-postlogin - user = dovenull + user = vmail vsz_limit = 256 M } service managesieve { @@ -244,11 +244,11 @@ userdb { } protocol imap { imap_metadata = yes - mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape #mail_crypt + mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl } mail_attribute_dict = file:%h/dovecot-attributes protocol lmtp { - mail_plugins = quota sieve acl zlib listescape #mail_crypt + mail_plugins = quota sieve acl zlib listescape mail_crypt mail_crypt_acl auth_socket_path = /usr/local/var/run/dovecot/auth-master } protocol sieve { @@ -288,9 +288,12 @@ plugin { sieve_before = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir sieve_after2 = /var/vmail/sieve/global.sieve - #mail_crypt_global_private_key = :
- + a-z A-Z - _ .
- +
- +
- +
-
+
:
@@ -76,12 +76,10 @@ $tfa_data = get_tfa();
- - - - @@ -125,12 +124,12 @@ $tfa_data = get_tfa(); @@ -156,13 +155,13 @@ $tfa_data = get_tfa();
- +
- +
@@ -202,7 +201,7 @@ $tfa_data = get_tfa();
- +
- +
- + @@ -328,17 +327,17 @@ $tfa_data = get_tfa();
- +
- +
- +
@@ -384,7 +383,7 @@ $tfa_data = get_tfa(); - + @@ -404,10 +403,10 @@ $tfa_data = get_tfa(); @@ -416,7 +415,7 @@ $tfa_data = get_tfa();
- +
- +
@@ -439,41 +438,41 @@ $tfa_data = get_tfa();
- +
- +
- +
/ - +
/ - +

- +
- +
- +
@@ -491,9 +490,9 @@ $tfa_data = get_tfa(); - [] - [whitelist] - [blacklist] + [] + [whitelist] + [blacklist] @@ -528,10 +527,10 @@ $tfa_data = get_tfa(); @@ -540,17 +539,17 @@ $tfa_data = get_tfa();
- +
- +
- +
- +
@@ -565,19 +564,19 @@ $tfa_data = get_tfa();
- +
- +

- @@ -587,7 +586,7 @@ $tfa_data = get_tfa(); ?>
- + @@ -649,19 +648,19 @@ $tfa_data = get_tfa();
- +
- +
- - + +

- +

@@ -755,21 +754,21 @@ $tfa_data = get_tfa();
- +
- +
- +
- +
@@ -791,8 +790,8 @@ echo "var pagination_size = '". $PAGINATION_SIZE . "';\n"; echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n"; ?> - - + + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, diff --git a/data/web/css/admin.css b/data/web/css/admin.css index a53d721c..bc89f1dd 100644 --- a/data/web/css/admin.css +++ b/data/web/css/admin.css @@ -65,12 +65,6 @@ body.modal-open { font-size:9pt; background:transparent; } -.bootstrap-select { - width: auto!important; -} .table-condensed .input-sm { width: 100%!important; } -.full-width-select { - width: 100%!important; -} diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index da2e96e3..488150d3 100644 --- a/data/web/css/mailbox.css +++ b/data/web/css/mailbox.css @@ -5,9 +5,6 @@ table.footable>tbody>tr.footable-empty>td { .pagination a { text-decoration: none !important; } -.panel panel-default { - overflow: visible !important; -} .btn-group { width: max-content; } diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css index 374688a1..04d1b874 100644 --- a/data/web/css/mailcow.css +++ b/data/web/css/mailcow.css @@ -148,3 +148,13 @@ nav .glyphicon { color: #5a5a5a; white-space: nowrap; } +.haveibeenpwned { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.full-width-select { + width: 100%!important; +} \ No newline at end of file diff --git a/data/web/debug.php b/data/web/debug.php index 646aaa19..d6616dcb 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -288,8 +288,8 @@ echo "var log_pagination_size = '". $LOG_PAGINATION_SIZE . "';\n"; ?> - - + +
- +
- +
- +
@@ -58,7 +58,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -92,7 +92,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- @@ -111,13 +111,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
- +
@@ -136,7 +136,31 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- + +
+
+ +
+
+
+

ACL

+
+
+
+ +
+
+ +
@@ -165,7 +189,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
- +
- +
- +
- +
- @@ -233,7 +257,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -256,17 +280,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
-
- +

@@ -278,17 +302,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
-
+
- +
-
- +
+ - +
@@ -300,17 +324,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
-
+
- +
-
- +
+ - +
@@ -338,7 +362,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -350,7 +374,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -358,17 +382,17 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
-
- +
- +
@@ -414,13 +438,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
max. MiB
- +
-
- +
- +
@@ -500,7 +524,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -512,17 +536,41 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
-
- + +
+
+
+ +
+
+
+

ACL

+
+
+
+ +
+
+
@@ -541,19 +589,19 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
- +
- +
@@ -565,7 +613,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -588,13 +636,13 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
- @@ -604,7 +652,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- @@ -625,7 +673,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -671,7 +719,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -683,7 +731,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- + +
+
+ + + + +

:

+
+
+ +
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+
@@ -885,7 +996,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -936,7 +1047,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
- +
@@ -965,14 +1076,14 @@ else { - - + + diff --git a/data/web/img/cow_mailcow.svg b/data/web/img/cow_mailcow.svg index d4577821..6ba98e46 100644 --- a/data/web/img/cow_mailcow.svg +++ b/data/web/img/cow_mailcow.svg @@ -1,5 +1,5 @@ - + image/svg+xml \ No newline at end of file + id="white_1_"> \ No newline at end of file diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index cdef8e0b..9e8daf7b 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -12,10 +12,20 @@ logger(); + + - - - - - - - - - - - - - - - -' : null; ?> -' : null; ?> -' : null; ?> -' : null; ?> -' : null; ?> -' : null; ?> - - + + + + + + <?=$UI_TEXTS['title_name'];?> + + + + + + + + + + + + + + + + + + ' : null; ?> + ' : null; ?> + ' : null; ?> + ' : null; ?> + ' : null; ?> + ' : null; ?> + + -
- -
+ + +
  • + +
  • ()
  • + + +
    +
    + +
    diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index c318d634..8ece2544 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "19082018_1004"; + $db_version = "03102018_1502"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -192,6 +192,26 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "tls_policy_override" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "dest" => "VARCHAR(255) NOT NULL", + "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL", + "parameters" => "VARCHAR(255) DEFAULT ''", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "unique" => array( + "dest" => array("dest") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "quarantine" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", @@ -280,10 +300,7 @@ function init_db_schema() { "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", - "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", - "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", - "recipient_maps" => "TINYINT(1) NOT NULL DEFAULT '0'", ), "keys" => array( "primary" => array( @@ -417,6 +434,32 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "da_acl" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", + "login_as" => "TINYINT(1) NOT NULL DEFAULT '1'", + "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", + "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", + "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + ), + "keys" => array( + "primary" => array( + "" => array("username") + ), + "fkey" => array( + "fk_domain_admin_acl" => array( + "col" => "username", + "ref" => "domain_admins.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "imapsync" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", @@ -950,8 +993,9 @@ DELIMITER ;'; 'msg' => 'db_init_complete' ); - // Fix user_acl + // Fix ACL $stmt = $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);"); + $stmt = $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);"); } catch (PDOException $e) { $_SESSION['return'][] = array( diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index a5e03672..4cb742af 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -35,7 +35,7 @@ $hrs = floor($mins / 60); $mins -= $hrs * 60; $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); -$dsn = $database_type . ":host=" . $database_host . ";dbname=" . $database_name; +$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name; $opt = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, @@ -46,12 +46,22 @@ try { $pdo = new PDO($dsn, $database_user, $database_pass, $opt); } catch (PDOException $e) { +// Stop when SQL connection fails ?> -
    Connection to database failed.

    The following error was reported:
    getMessage();?>
    +
    Connection to database failed.

    The following error was reported:
    getMessage();?>
    +
    Connection to dockerapi container failed.

    The following error was reported:
    -
    + 'danger', @@ -124,6 +134,7 @@ if (isset($_GET['lang']) && in_array($_GET['lang'], $AVAILABLE_LANGUAGES)) { require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php'; include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.acl.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.address_rewriting.inc.php'; @@ -135,12 +146,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rsettings.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php'; init_db_schema(); if (isset($_SESSION['mailcow_cc_role'])) { - set_acl(); + acl('to_session'); } $UI_TEXTS = customize('get', 'ui_texts'); + diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 4df1beca..d37c0968 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -40,7 +40,7 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { } } -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") { +if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['acl']['login_as'] == "1") { if (isset($_GET["duallogin"])) { $duallogin = html_entity_decode(rawurldecode($_GET["duallogin"])); if (filter_var($duallogin, FILTER_VALIDATE_EMAIL)) { diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 4ac0df47..5cf2ea94 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -9,6 +9,7 @@ This file will be reset on upgrades. // SQL database connection variables $database_type = 'mysql'; +$database_sock = '/var/run/mysqld/mysqld.sock'; $database_host = 'mysql'; $database_user = getenv('DBUSER'); $database_pass = getenv('DBPASS'); @@ -122,3 +123,12 @@ $DOCKER_TIMEOUT = 60; // Anonymize IPs logged via UI $ANONYMIZE_IPS = true; + +// Force incoming TLS for new mailboxes by default +$MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in'] = false; + +// Force outgoing TLS for new mailboxes by default +$MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false; + +// Force password change on next login (only allows login to mailcow UI) +$MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false; diff --git a/data/web/index.php b/data/web/index.php index 47339923..c839ea86 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -2,15 +2,15 @@ require_once 'inc/prerequisites.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin.php'); + header('Location: /admin'); exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /mailbox.php'); + header('Location: /mailbox'); exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user.php'); + header('Location: /user'); exit(); } require_once 'inc/header.inc.php'; @@ -107,6 +107,6 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; - + ' + ' Test' + - ' ' + lang.edit + '' + - ' ' + lang.remove + '' + + ' ' + lang.edit + '' + + ' ' + lang.remove + '' + ''; item.chkbox = ''; }); } else if (table == 'forwardinghoststable') { $.each(data, function (i, item) { item.action = ''; if (item.keep_spam == "yes") { item.keep_spam = lang.no; @@ -140,8 +144,8 @@ jQuery(function($){ item.selected_domains = escapeHtml(item.selected_domains.toString().replace(/,/g, " ")); item.chkbox = ''; item.action = ''; }); diff --git a/data/web/js/api.js b/data/web/js/api.js index 2e770f7b..e8293dbc 100644 --- a/data/web/js/api.js +++ b/data/web/js/api.js @@ -5,9 +5,9 @@ $(document).ready(function() { } else { var parent_btn_grp = $(elem).parentsUntil(".btn-group").parent(); if (parent_btn_grp.hasClass('btn-group')) { - parent_btn_grp.replaceWith(' + + +

    +
    +
    +
    +
    +
    + + + + +
    +
    + + @@ -304,14 +327,17 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/mailbox.php'; - - + +
    - +
    - +
    @@ -35,7 +35,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
    - +
    @@ -60,14 +60,14 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
    - + a-z A-Z - _ .
    - ".htmlspecialchars($domain).""; @@ -79,13 +79,13 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
    - +
    - +
    @@ -97,7 +97,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
    - +
    @@ -115,11 +115,11 @@ if (!isset($_SESSION['mailcow_cc_role'])) {