diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..24874a2540 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,29 @@ +**Notice (you can delete this paragraph)** +Please understand that we use GitHub as **bug tracker and for feature requests only**. +For general (community) support and other discussion, you are welcome to visit us @ Freenode, #mailcow +Answering may take from a few seconds to hours, please be patient. :-) +Immediate commercial support is available via mailcow@incidents.servercow.de + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System** + - OS: [e.g. iOS] + - Browser: [e.g. chrome, safari] + - Other clients involved: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..24db8c039a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,18 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - enhancement +# Label to use when marking an issue as stale +staleLabel: dunno +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.gitignore b/.gitignore index e535c710e7..18df41c28d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ data/conf/nginx/*.conf data/conf/nginx/*.custom data/conf/nginx/*.bak data/conf/dovecot/extra.conf +data/conf/rspamd/custom/* +data/conf/portainer/ +data/gitea/ +data/gogs/ +.github/ +docker-compose.override.yml diff --git a/.travis.yml b/.travis.yml index 8435709078..c4ab581a45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,9 @@ sudo: required services: - docker script: +- sed -i '/oom_kill_disable/d' docker-compose.yml - echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh -- docker-compose pull --ignore-pull-failures --parallel - docker-compose build -- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD -- docker-compose push branches: only: - - master_disabled -env: - global: - - secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w= - - secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo= + - travis diff --git a/README.md b/README.md index 42b2a3f1ba..ec500d9c0d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # mailcow: dockerized - 🐮 + 🐋 = 💕 -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) +## Want to support mailcow? -**mailcow Bitcoin donations:** 1E5rgzgA1sS3QH7r1ToWxRC3GEavfsGMrx +Donate via **PayPal** [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) or via **Liberapay** [![Liberapay.com](https://mailcow.email/img/lp.png)](https://liberapay.com/mailcow) + +Or just spread the word: moo. + +## Info and documentation Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index 7f4f1fcbe3..0c2099077c 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -13,8 +13,12 @@ log_f() { elif [[ ${2} != "redis_only" ]]; then echo "$(date) - ${1}" fi - redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ - tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null + if [[ ${3} == "b64" ]]; then + redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${1}")\"}" > /dev/null + else + redis-cli -h redis LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ + tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null + fi } if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then @@ -37,7 +41,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 } @@ -66,12 +70,8 @@ get_ipv4(){ local IPV4= local IPV4_SRCS= local TRY= - IPV4_SRCS[0]="api.ipify.org" - IPV4_SRCS[1]="ifconfig.co"- - IPV4_SRCS[2]="icanhazip.com" - IPV4_SRCS[3]="v4.ident.me" - IPV4_SRCS[4]="ipecho.net/plain" - IPV4_SRCS[5]="ip4.mailcow.email" + IPV4_SRCS[0]="ip4.mailcow.email" + IPV4_SRCS[1]="ip4.korves.net" until [[ ! -z ${IPV4} ]] || [[ ${TRY} -ge 10 ]]; do IPV4=$(curl --connect-timeout 3 -m 10 -L4s ${IPV4_SRCS[$RANDOM % ${#IPV4_SRCS[@]} ]} | grep -E "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") [[ ! -z ${TRY} ]] && sleep 1 @@ -84,10 +84,8 @@ get_ipv6(){ local IPV6= local IPV6_SRCS= local TRY= - IPV6_SRCS[0]="ifconfig.co" - IPV6_SRCS[1]="icanhazip.com" - IPV6_SRCS[2]="v6.ident.me" - IPV6_SRCS[3]="ip6.mailcow.email" + IPV6_SRCS[0]="ip6.korves.net" + IPV6_SRCS[1]="ip6.mailcow.email" until [[ ! -z ${IPV6} ]] || [[ ${TRY} -ge 10 ]]; do IPV6=$(curl --connect-timeout 3 -m 10 -L6s ${IPV6_SRCS[$RANDOM % ${#IPV6_SRCS[@]} ]} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") [[ ! -z ${TRY} ]] && sleep 1 @@ -131,7 +129,7 @@ else fi log_f "Waiting for database... " -while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do +while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do sleep 2 done log_f "Initializing, please wait... " @@ -153,20 +151,33 @@ while true; do IPV6=$(get_ipv6) log_f "OK" no_date + # Hard-fail on CAA errors for MAILCOW_HOSTNAME + MH_PARENT_DOMAIN=$(echo ${MAILCOW_HOSTNAME} | cut -d. -f2-) + MH_CAAS=( $(dig CAA ${MH_PARENT_DOMAIN} +short | sed -n 's/\d issue "\(.*\)"/\1/p') ) + if [[ ! -z ${MH_CAAS} ]]; then + if [[ ${MH_CAAS[@]} =~ "letsencrypt.org" ]]; then + echo "Validated CAA for parent domain ${MH_PARENT_DOMAIN}" + else + echo "Skipping ACME validation: Lets Encrypt disallowed for ${MAILCOW_HOSTNAME} by CAA record, retrying in 1h..." + sleep 1h + exec $(readlink -f "$0") + fi + 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) @@ -249,6 +260,17 @@ while true; do fi for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do + # Skip on CAA errors for SAN + SAN_PARENT_DOMAIN=$(echo ${SAN} | cut -d. -f2-) + SAN_CAAS=( $(dig CAA ${SAN_PARENT_DOMAIN} +short | sed -n 's/\d issue "\(.*\)"/\1/p') ) + if [[ ! -z ${SAN_CAAS} ]]; then + if [[ ${SAN_CAAS[@]} =~ "letsencrypt.org" ]]; then + echo "Validated CAA for parent domain ${SAN_PARENT_DOMAIN} of ${SAN}" + else + echo "Skipping ACME validation for ${SAN}: Lets Encrypt disallowed for ${SAN} by CAA record" + continue + fi + fi if [[ ${SAN} == ${MAILCOW_HOSTNAME} ]]; then continue fi @@ -306,10 +328,10 @@ while true; do -k ${ACME_BASE}/acme/private/privkey.pem \ -c ${ACME_BASE}/acme \ ${ALL_VALIDATED[*]} 2>&1 | tee /dev/fd/5) - case "$?" in 0) # new certs - log_f "${ACME_RESPONSE}" redis_only + ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) + log_f "${ACME_RESPONSE_B64}" redis_only b64 # cp the new certificates and keys cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/key.pem @@ -323,7 +345,8 @@ while true; do restart_containers ${CONTAINERS_RESTART[*]} ;; 1) # failure - log_f "${ACME_RESPONSE}" redis_only + ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) + log_f "${ACME_RESPONSE_B64}" redis_only b64 if [[ $ACME_RESPONSE =~ "No registration exists" ]]; then log_f "Registration keys are invalid, deleting old keys and restarting..." rm ${ACME_BASE}/acme/private/account.key @@ -352,7 +375,8 @@ while true; do exec $(readlink -f "$0") ;; 2) # no change - log_f "${ACME_RESPONSE}" redis_only + ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) + log_f "${ACME_RESPONSE_B64}" redis_only b64 if ! diff ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem; then log_f "Certificate was not changed, but active certificate does not match the verified certificate, fixing and restarting containers..." cp ${ACME_BASE}/acme/fullchain.pem ${ACME_BASE}/cert.pem @@ -369,7 +393,8 @@ while true; do [[ ${TRIGGER_RESTART} == 1 ]] && restart_containers ${CONTAINERS_RESTART[*]} ;; *) # unspecified - log_f "${ACME_RESPONSE}" redis_only + ACME_RESPONSE_B64=$(echo ${ACME_RESPONSE} | openssl enc -e -A -base64) + log_f "${ACME_RESPONSE_B64}" redis_only b64 if [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ]] && [[ -f ${ACME_BASE}/acme/private/${DATE}.bak/privkey.pem ]]; then log_f "Error requesting certificate, restoring previous certificate from backup and restarting containers...." cp ${ACME_BASE}/acme/private/${DATE}.bak/fullchain.pem ${ACME_BASE}/cert.pem diff --git a/data/Dockerfiles/clamd/bootstrap.sh b/data/Dockerfiles/clamd/bootstrap.sh index 76a6b79bd4..ba1cee85a1 100755 --- a/data/Dockerfiles/clamd/bootstrap.sh +++ b/data/Dockerfiles/clamd/bootstrap.sh @@ -14,6 +14,10 @@ chown root:tty /dev/console chmod g+rw /dev/console # Prepare +[[ ! -f /var/lib/clamav/whitelist.ign2 ]] && touch /var/lib/clamav/whitelist.ign2 +dos2unix /var/lib/clamav/whitelist.ign2 +sed -i '/^\s*$/d' /var/lib/clamav/whitelist.ign2 + BACKGROUND_TASKS=() ( diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 78389c4f4d..9d0bdaec7e 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 e66cf23820..a161c8a693 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -3,12 +3,16 @@ 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__) @@ -82,33 +86,85 @@ def post(self, container_id, post_action): elif request.json['cmd'] == 'sieve_list' and request.json['username']: try: for container in docker_client.containers.list(filters={"id": container_id}): - sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"]) return sieve_return.output except Exception as e: return jsonify(type='danger', msg=str(e)) elif request.json['cmd'] == 'sieve_print' and request.json['script_name'] and request.json['username']: try: for container in docker_client.containers.list(filters={"id": container_id}): - sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"], user='vmail') + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"]) 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 @@ def __init__(self): 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 0332391409..fdfa84f55b 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 70ffb701b1..d45322b71d 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 status --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,15 +107,17 @@ 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 +if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem @@ -128,6 +131,9 @@ sievec /usr/local/lib/dovecot/sieve/report-spam.sieve sievec /usr/local/lib/dovecot/sieve/report-ham.sieve # Fix permissions +chown root:root /usr/local/etc/dovecot/sql/*.conf +chown root:dovecot /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve* /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota* +chmod 640 /usr/local/etc/dovecot/sql/*.conf chown -R vmail:vmail /var/vmail/sieve # Fix more than 1 hardlink issue @@ -138,7 +144,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 0000000000..24c1e46144 --- /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 343910ff9a..01a45f31f1 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 9d961be032..9b26817cda 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 3b9e34972f..d06aa91964 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 0000000000..0cea240c17 --- /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 b8da97400c..8489f27ae1 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 3acfd09fd1..0ed81896de 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 44d46f741c..694d5cf6f0 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -4,25 +4,31 @@ 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 status --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 +# Trigger db init +echo "Running DB init..." +php -c /usr/local/etc/php -f /web/inc/init_db.inc.php + # Migrate domain map declare -a DOMAIN_ARR 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,10 +54,9 @@ 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 -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}"; + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +DELETE FROM api; +INSERT INTO api (api_key, active, allow_from) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}"); EOF fi fi diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 28e51ccdc1..83ec94e08f 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' @@ -244,6 +252,8 @@ chmod 700 /var/lib/zeyple/keys chown -R 600:600 /var/lib/zeyple/keys # Fix Postfix permissions +chown -R root:postfix /opt/postfix/conf/sql/ +chmod 640 /opt/postfix/conf/sql/*.cf chgrp -R postdrop /var/spool/postfix/public chgrp -R postdrop /var/spool/postfix/maildrop postfix set-permissions diff --git a/data/Dockerfiles/postfix/rspamd-pipe-ham b/data/Dockerfiles/postfix/rspamd-pipe-ham index 9d961be032..9b26817cda 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 3b9e34972f..d06aa91964 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 67b8aa1cd3..daf9760b0c 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 afb03bb663..6288550d35 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 a9abd901ad..0000000000 --- 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 839ec5c6cd..0000000000 --- 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 7765f08cae..9d3965a724 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ netcat \ psmisc \ wget \ + patch \ && rm -rf /var/lib/apt/lists/* \ && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ @@ -45,7 +46,9 @@ COPY supervisord.conf /etc/supervisor/supervisord.conf COPY theme-blue.js /usr/lib/GNUstep/SOGo/WebServerResources/js/theme-blue.js COPY theme-blue.css /usr/lib/GNUstep/SOGo/WebServerResources/css/theme-default.css 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/acl.diff b/data/Dockerfiles/sogo/acl.diff new file mode 100644 index 0000000000..5137003886 --- /dev/null +++ b/data/Dockerfiles/sogo/acl.diff @@ -0,0 +1,11 @@ +--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200 ++++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200 +@@ -46,7 +46,7 @@ + + + +- + diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh index de995c2285..b8dc895c88 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 status --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 ' @@ -172,4 +172,27 @@ sed -i \ /usr/lib/GNUstep/SOGo/WebServerResources/js/Common/Common.app.js \ /usr/lib/GNUstep/SOGo/WebServerResources/js/Common.js +sed -i \ + -e 's/default: "900"/default: "700"/g' \ + -e 's/default: "500"/default: "700"/g' \ + -e 's/"hue-1": "400"/"hue-1": "500"/g' \ + -e 's/"hue-1": "A100"/"hue-1": "500"/g' \ + -e 's/"hue-2": "800"/"hue-2": "700"/g' \ + -e 's/"hue-2": "300"/"hue-2": "700"/g' \ + -e 's/"hue-3": "A700"/"hue-3": "A200"/' \ + -e 's/default:"900"/default:"700"/g' \ + -e 's/default:"500"/default:"700"/g' \ + -e 's/"hue-1":"400"/"hue-1":"500"/g' \ + -e 's/"hue-1":"A100"/"hue-1":"500"/g' \ + -e 's/"hue-2":"800"/"hue-2":"700"/g' \ + -e 's/"hue-2":"300"/"hue-2":"700"/g' \ + -e 's/"hue-3":"A700"/"hue-3":"A200"/' \ + /usr/lib/GNUstep/SOGo/WebServerResources/js/Common/Common.app.js \ + /usr/lib/GNUstep/SOGo/WebServerResources/js/Common.js + +# Patch ACLs (comment this out to enable any or authenticated targets for ACL) +if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then + patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +fi + exec gosu sogo /usr/sbin/sogod diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index 2a8895602c..1c1422b3b8 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 72e86bc096..8e056a935b 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.6 +FROM alpine:3.8 LABEL maintainer "Andre Peters " @@ -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 b458cd8a6e..d179eacab3 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 c06abbc244..ab528a78cf 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 0000000000..94e394ac6d --- /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/clamav/whitelist.ign2 b/data/conf/clamav/whitelist.ign2 new file mode 100644 index 0000000000..70ef98b9f3 --- /dev/null +++ b/data/conf/clamav/whitelist.ign2 @@ -0,0 +1 @@ +Example-Signature.Ignore-1 diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 820a825b4b..02ce5dff43 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/:INDEX=~/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,24 +244,26 @@ 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 { managesieve_logout_format = bytes=%i/%o } plugin { - acl_anyone = allow + # Allow "any" or "authenticated" to be used in ACLs + #acl_anyone = allow acl_shared_dict = file:/var/vmail/shared-mailboxes.db acl = vfile quota = dict:Userquota::proxy::sqlquota quota_rule2 = Trash:storage=+100%% sieve = /var/vmail/sieve/%u.sieve sieve_plugins = sieve_imapsieve sieve_extprograms + sieve_vacation_send_from_recipient = yes # From elsewhere to Spam folder imapsieve_mailbox1_name = Junk imapsieve_mailbox1_causes = COPY @@ -286,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 = Get('Q_MAX_SIZE')) { - if (!empty($max_size) && ($max_size * 1048576) < $raw_size) { - error_log(sprintf("Message too large: %d exceeds %d", $raw_size, ($max_size * 1048576))); - http_response_code(505); - exit; - } + $max_size = (int)$redis->Get('Q_MAX_SIZE'); + if (($max_size * 1048576) < $raw_size) { + error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576))); + http_response_code(505); + exit; } if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) { $exclude_domains = json_decode($exclude_domains, true); @@ -73,7 +74,7 @@ function getallheaders() { $retention_size = (int)$redis->Get('Q_RETENTION_SIZE'); } catch (RedisException $e) { - error_log($e); + error_log("QUARANTINE: " . $e); http_response_code(504); exit; } @@ -92,14 +93,14 @@ function getallheaders() { } } catch (RedisException $e) { - error_log($e); + error_log("QUARANTINE: " . $e); http_response_code(504); exit; } // Skip if domain is excluded if (in_array($parsed_rcpt['domain'], $exclude_domains)) { - error_log(sprintf("Skipped domain %s", $parsed_rcpt['domain'])); + error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain'])); continue; } @@ -134,12 +135,12 @@ function getallheaders() { // Loop through all found gotos foreach ($gotos_array as $index => &$goto) { - error_log("quarantine pipe: query " . $goto . " as username from mailbox"); + error_log("QUARANTINE: quarantine pipe: query " . $goto . " as username from mailbox"); $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND `active`= '1';"); $stmt->execute(array(':goto' => $goto)); $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; if (!empty($username)) { - error_log("quarantine pipe: mailbox found: " . $username); + error_log("QUARANTINE: quarantine pipe: mailbox found: " . $username); // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate if (!in_array($username, $rcpt_final_mailboxes)) { $rcpt_final_mailboxes[] = $username; @@ -148,13 +149,13 @@ function getallheaders() { else { $parsed_goto = parse_email($goto); if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { - error_log($goto . " is not a mailcow handled mailbox or alias address"); + error_log("QUARANTINE:" . $goto . " is not a mailcow handled mailbox or alias address"); } else { $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); $stmt->execute(array(':goto' => $goto)); $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; - error_log("quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); + error_log("QUARANTINE: quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); $goto_branch_array = explode(',', $goto_branch); } } @@ -174,18 +175,18 @@ function getallheaders() { // Force exit if loop cannot be solved // Postfix does not allow for alias loops, so this should never happen. $loop_c++; - error_log("quarantine pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array)); + error_log("QUARANTINE: quarantine pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array)); } } catch (PDOException $e) { - error_log($e->getMessage()); + error_log("QUARANTINE: " . $e->getMessage()); http_response_code(502); exit; } } foreach ($rcpt_final_mailboxes as $rcpt) { - error_log("quarantine pipe: processing quarantine message for rcpt " . $rcpt); + error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt); try { $stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`) VALUES (:qid, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action)"); @@ -217,7 +218,7 @@ function getallheaders() { )); } catch (PDOException $e) { - error_log($e->getMessage()); + error_log("QUARANTINE: " . $e->getMessage()); http_response_code(503); exit; } diff --git a/data/conf/rspamd/override.d/worker-controller.inc b/data/conf/rspamd/override.d/worker-controller.inc index 60338e15b5..4d4ffe6f93 100644 --- a/data/conf/rspamd/override.d/worker-controller.inc +++ b/data/conf/rspamd/override.d/worker-controller.inc @@ -1,7 +1,7 @@ bind_socket = "*:11334"; -count = 2; +count = 1; secure_ip = "127.0.0.1"; secure_ip = "::1"; -bind_socket = "/rspamd-sock/rspamd.sock mode=0666 owner=nobody"; +bind_socket = "/var/lib/rspamd/rspamd.sock mode=0666 owner=nobody"; .include(try=true; priority=10) "$CONFDIR/override.d/worker-controller-password.inc" .include(try=true; priority=20) "$CONFDIR/override.d/worker-controller.custom.inc" diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf index 23fd82401e..b5979c826d 100644 --- a/data/conf/sogo/sogo.conf +++ b/data/conf/sogo/sogo.conf @@ -39,17 +39,17 @@ SxVMemLimit = 384; - SOGoMaximumPingInterval = 354; + SOGoMaximumPingInterval = 3540; - SOGoInternalSyncInterval = 30; - SOGoMaximumSyncInterval = 354; + SOGoInternalSyncInterval = 45; + SOGoMaximumSyncInterval = 3540; // 100 seems to break some Android clients - //SOGoMaximumSyncWindowSize = 100; + //SOGoMaximumSyncWindowSize = 99; // This should do the trick for Outlook 2016 - SOGoMaximumSyncResponseSize = 2048; + SOGoMaximumSyncResponseSize = 512; - WOWatchDogRequestTimeout = 10; + WOWatchDogRequestTimeout = 20; WOListenQueueSize = 300; WONoDetach = YES; diff --git a/data/conf/unbound/unbound.conf b/data/conf/unbound/unbound.conf index b3c7767123..af725f0fc1 100644 --- a/data/conf/unbound/unbound.conf +++ b/data/conf/unbound/unbound.conf @@ -2,28 +2,39 @@ server: verbosity: 1 interface: 0.0.0.0 interface: ::0 - logfile: /dev/stdout + logfile: /dev/console do-ip4: yes do-ip6: yes do-udp: yes do-tcp: yes do-daemonize: no + #access-control: 0.0.0.0/0 allow access-control: 10.0.0.0/8 allow access-control: 172.16.0.0/12 allow access-control: 192.168.0.0/16 allow access-control: fc00::/7 allow access-control: fe80::/10 allow + #access-control: ::0/0 allow directory: "/etc/unbound" username: unbound auto-trust-anchor-file: trusted-key.key - private-address: 10.0.0.0/8 - private-address: 172.16.0.0/12 - private-address: 192.168.0.0/16 - private-address: 169.254.0.0/16 - private-address: fc00::/7 - private-address: fe80::/10 + #private-address: 10.0.0.0/8 + #private-address: 172.16.0.0/12 + #private-address: 192.168.0.0/16 + #private-address: 169.254.0.0/16 + #private-address: fc00::/7 + #private-address: fe80::/10 root-hints: "/etc/unbound/root.hints" hide-identity: yes hide-version: yes max-udp-size: 4096 msg-buffer-size: 65552 + +remote-control: + control-enable: yes + control-interface: 127.0.0.1 + control-port: 8953 + server-key-file: "/etc/unbound/unbound_server.key" + server-cert-file: "/etc/unbound/unbound_server.pem" + control-key-file: "/etc/unbound/unbound_control.key" + control-cert-file: "/etc/unbound/unbound_control.pem" diff --git a/data/web/admin.php b/data/web/admin.php index ce3deb69ad..a4d7b697f4 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -1,8 +1,8 @@ @@ -10,6 +10,7 @@
@@ -17,34 +18,28 @@
-
- -
- -
- - ↳ a-z A-Z - _ . -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
+
+
+
+
+
+ + + +
- -
+
+ + + +
:
@@ -68,7 +63,7 @@
:
- @@ -76,29 +71,30 @@
-
-
- -
@@ -118,19 +115,19 @@
-
+
@@ -156,13 +153,13 @@
- +
- +
@@ -202,12 +199,14 @@
- +
@@ -229,7 +228,7 @@ else { ?>
-
+

Domain:

@@ -240,6 +239,8 @@ } foreach(mailbox('get', 'alias_domains', $domain) as $alias_domain) { if (!empty($dkim = dkim('details', $alias_domain))) { + $dkim_domains[] = $alias_domain; + ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.'); ?>
@@ -261,7 +262,7 @@ else { ?>
-
+

↳ Alias-Domain:

@@ -274,6 +275,8 @@ } foreach(dkim('blind') as $blind) { if (!empty($dkim = dkim('details', $blind))) { + $dkim_domains[] = $blind; + ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.'); ?>
@@ -298,40 +301,90 @@
- - + + +
- +
-
- +
- + + +
- +
- +
- +
- +
+ + + + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
@@ -348,10 +401,10 @@
@@ -360,7 +413,7 @@
- +
- +
@@ -383,41 +436,41 @@
- +
- +
- +
/ - +
/ - +

- +
- +
- +
@@ -435,9 +488,9 @@ - [] - [whitelist] - [blacklist] + [] + [whitelist] + [blacklist] @@ -472,10 +525,10 @@
@@ -484,17 +537,17 @@
- +
- +
- +
- +
@@ -509,19 +562,19 @@
- +
- +

- @@ -531,7 +584,7 @@ ?>
- + @@ -593,19 +646,19 @@
- +
- +
- - + +

- +

@@ -699,21 +752,21 @@
- +
- +
- +
- +
@@ -721,6 +774,88 @@ +
+
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ + +
+
+ + +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
- - + + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, @@ -74,7 +75,7 @@ $redis->lTrim('AUTODISCOVER_LOG', 0, 100); } catch (RedisException $e) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', 'msg' => 'Redis: '.$e ); @@ -128,7 +129,7 @@ $redis->lTrim('AUTODISCOVER_LOG', 0, 100); } catch (RedisException $e) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'danger', 'msg' => 'Redis: '.$e ); diff --git a/data/web/css/admin.css b/data/web/css/admin.css index bd6ce1a5a2..b1871d87d3 100644 --- a/data/web/css/admin.css +++ b/data/web/css/admin.css @@ -65,9 +65,9 @@ body.modal-open { font-size:9pt; background:transparent; } -.bootstrap-select { - width: auto!important; -} .table-condensed .input-sm { width: 100%!important; } +.table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { + padding: 3px; +} \ No newline at end of file diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index da2e96e362..488150d39a 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 56d129dde6..04d1b87449 100644 --- a/data/web/css/mailcow.css +++ b/data/web/css/mailcow.css @@ -101,6 +101,7 @@ legend { -ms-user-select: none -o-user-select: none; user-select: none; + font-size: 12pt; } .navbar .navbar-brand { padding-top: 5px; @@ -147,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 646aaa197f..d3d7d794f8 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -1,8 +1,8 @@ @@ -288,8 +288,8 @@ ?> - - + +
@@ -18,119 +18,47 @@ -

-
-
- -
- -
- -
- -
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- - - -

+ if (isset($_GET["alias"]) && + !empty($_GET["alias"])) { + $alias = html_entity_decode(rawurldecode($_GET["alias"])); + $result = mailbox('get', 'alias_details', $alias); + if (!empty($result)) { + ?> +


-
+
- +
- +
- +
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
+
- + +
+
+ +
+
+
- +
- +
@@ -142,12 +70,165 @@ +

+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+

ACL

+
+
+
+ +
+
+ +
+
+
+
+ + + +

+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + @@ -159,7 +240,7 @@
- +
- +
- +
- +
- +
- @@ -227,7 +308,7 @@
- +
@@ -250,17 +331,17 @@
- +
-
- +

@@ -272,17 +353,17 @@
-
+
- +
-
- +
+ - +
@@ -294,17 +375,17 @@
-
+
- +
-
- +
+ - +
@@ -323,7 +404,7 @@ !empty($_GET["aliasdomain"])) { $alias_domain = html_entity_decode(rawurldecode($_GET["aliasdomain"])); $result = mailbox('get', 'alias_domain_details', $alias_domain); - $rl = mailbox('get', 'ratelimit', $alias_domain); + $rl = ratelimit('get', 'domain', $alias_domain); if (!empty($result)) { ?>

@@ -332,7 +413,7 @@
- +
@@ -344,7 +425,7 @@
- +
@@ -352,17 +433,17 @@
- +
-
- +

@@ -400,7 +481,7 @@
- +
@@ -408,13 +489,13 @@
max. MiB
- +
-
- +
- +
@@ -494,7 +575,7 @@
- +
@@ -506,17 +587,41 @@
- +
-
- + +
+
+
+ +
+
+
+

ACL

+
+
+
+ +
+
+
@@ -535,19 +640,19 @@
- +
- +
- +
@@ -559,7 +664,7 @@
- +
@@ -582,13 +687,13 @@
- +
- @@ -598,7 +703,7 @@
- @@ -619,7 +724,7 @@
- +
@@ -636,23 +741,23 @@ $result = bcc('details', $bcc); if (!empty($result)) { ?> -

BCC map

+


- +
- - BCC destination must be a single valid email address. + +
- +
@@ -665,7 +770,7 @@
- +
@@ -677,7 +782,9 @@
- + +
+
+ + + + +

:

+
+
+ +
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+
@@ -879,7 +1047,7 @@
- +
@@ -930,7 +1098,7 @@
- +
@@ -959,14 +1127,14 @@ - - + + diff --git a/data/web/img/cow_mailcow.svg b/data/web/img/cow_mailcow.svg index bea3f82630..6ba98e4660 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/ajax/dns_diagnostics.php b/data/web/inc/ajax/dns_diagnostics.php index 45ff07e2b2..dd67dec943 100644 --- a/data/web/inc/ajax/dns_diagnostics.php +++ b/data/web/inc/ajax/dns_diagnostics.php @@ -74,7 +74,7 @@ // Init records array $spf_link = 'SPF Record Syntax
'; -$dmarc_link = 'DMARC Assistant'; +$dmarc_link = 'DMARC Assistant'; $records = array(); if ($_SESSION['mailcow_cc_role'] == "admin") { diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index d7b8072329..9e8daf7b81 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -12,10 +12,20 @@ + + - - - - - - - - - - - - - - - -' : 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 a3b67283e6..f2db0f1a1f 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 = "31072018_2319"; + $db_version = "07102018_1502"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -76,7 +76,7 @@ function init_db_schema() { "c_cn" => "VARCHAR(255)", "mail" => "VARCHAR(255) NOT NULL", // TODO -> use TEXT and check if SOGo login breaks on empty aliases - "aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", + "aliases" => "TEXT NOT NULL", "ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", "home" => "VARCHAR(255)", "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", @@ -112,6 +112,7 @@ function init_db_schema() { ), "alias" => array( "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", "address" => "VARCHAR(255) NOT NULL", "goto" => "TEXT NOT NULL", "domain" => "VARCHAR(255) NOT NULL", @@ -121,7 +122,10 @@ function init_db_schema() { ), "keys" => array( "primary" => array( - "" => array("address") + "" => array("id") + ), + "unique" => array( + "address" => array("address") ), "key" => array( "domain" => array("domain") @@ -131,7 +135,6 @@ function init_db_schema() { ), "api" => array( "cols" => array( - "username" => "VARCHAR(255) NOT NULL", "api_key" => "VARCHAR(255) NOT NULL", "allow_from" => "VARCHAR(512) NOT NULL", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", @@ -140,16 +143,8 @@ function init_db_schema() { ), "keys" => array( "primary" => array( - "" => array("username") + "" => array("api_key") ), - "fkey" => array( - "fk_username_api" => array( - "col" => "username", - "ref" => "admin.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), @@ -188,6 +183,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", @@ -276,10 +291,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( @@ -366,12 +378,13 @@ function init_db_schema() { "logs" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", + "task" => "CHAR(32) NOT NULL DEFAULT '000000'", "type" => "VARCHAR(32) DEFAULT ''", "msg" => "TEXT", "call" => "TEXT", "user" => "VARCHAR(64) NOT NULL", "role" => "VARCHAR(32) NOT NULL", - "remote" => "VARCHAR(32) NOT NULL", + "remote" => "VARCHAR(39) NOT NULL", "time" => "INT(11) NOT NULL" ), "keys" => array( @@ -412,6 +425,24 @@ 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") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "imapsync" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", @@ -754,6 +785,12 @@ function init_db_schema() { if ($num_results == 0) { if (strpos($type, 'AUTO_INCREMENT') !== false) { $type = $type . ' PRIMARY KEY '; + // Adding an AUTO_INCREMENT key, need to drop primary keys first, if exists + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); + } } $pdo->query("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type); } @@ -933,21 +970,49 @@ function init_db_schema() { WHERE `username` = :username"); $stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user)); } - $_SESSION['return'] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__), - 'msg' => 'db_init_complete' - ); - - // Fix user_acl + if (php_sapi_name() == "cli") { + echo "DB initialization completed" . PHP_EOL; + } else { + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__), + 'msg' => 'db_init_complete' + ); + } + // 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`);"); + // Fix domain_admins + $stmt = $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';"); } catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__), - 'msg' => array('mysql_error', $e) - ); + if (php_sapi_name() == "cli") { + echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; + } else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__), + 'msg' => array('mysql_error', $e) + ); + } } } -?> +if (php_sapi_name() == "cli") { + include '/web/inc/vars.inc.php'; + $now = new DateTime(); + $mins = $now->getOffset() / 60; + $sgn = ($mins < 0 ? -1 : 1); + $mins = abs($mins); + $hrs = floor($mins / 60); + $mins -= $hrs * 60; + $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); + $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, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;", + ]; + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + init_db_schema(); +} diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index ad201a687b..67f72b78c5 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -35,7 +35,7 @@ $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,11 +46,66 @@ $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', + 'log' => array(__FUNCTION__), + 'msg' => array('mysql_error', $e) + ); + return false; + } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__), + 'msg' => array('mysql_error', 'unknown error') + ); + return false; + } +} +set_exception_handler('pdo_exception_handler'); + +// TODO: Move function +function get_remote_ip($anonymize = null) { + global $ANONYMIZE_IPS; + if ($anonymize === null) { + $anonymize = $ANONYMIZE_IPS; + } + elseif ($anonymize !== true && $anonymize !== false) { + $anonymize = true; + } + $remote = $_SERVER['REMOTE_ADDR']; + if (filter_var($remote, FILTER_VALIDATE_IP) === false) { + return '0.0.0.0'; + } + if ($anonymize) { + if (strlen(inet_pton($remote)) == 4) { + return inet_ntop(inet_pton($remote) & inet_pton("255.255.255.0")); + } + elseif (strlen(inet_pton($remote)) == 16) { + return inet_ntop(inet_pton($remote) & inet_pton('ffff:ffff:ffff:ffff:0000:0000:0000:0000')); + } + } + else { + return $remote; + } +} require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; @@ -79,22 +134,27 @@ 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'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantine.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.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/sessions.inc.php b/data/web/inc/sessions.inc.php index fe1cdfa2b8..ac77e9ae5a 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -28,16 +28,16 @@ // API if (!empty($_SERVER['HTTP_X_API_KEY'])) { - $stmt = $pdo->prepare("SELECT `username`, `allow_from` FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); + $stmt = $pdo->prepare("SELECT `allow_from` FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); $stmt->execute(array( - ':api_key' => preg_replace('/[^A-Z0-9-]/i', '', $_SERVER['HTTP_X_API_KEY']) + ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY']) )); $api_return = $stmt->fetch(PDO::FETCH_ASSOC); if (!empty($api_return['username'])) { $remote = get_remote_ip(false); $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); if (in_array($remote, $allow_from)) { - $_SESSION['mailcow_cc_username'] = $api_return['username']; + $_SESSION['mailcow_cc_username'] = 'API'; $_SESSION['mailcow_cc_role'] = 'admin'; $_SESSION['mailcow_cc_api'] = true; } @@ -52,7 +52,7 @@ function session_check() { return true; } if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'warning', 'msg' => 'session_ua' ); @@ -60,7 +60,7 @@ function session_check() { } if (!empty($_POST)) { if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { - $_SESSION['return'] = array( + $_SESSION['return'][] = array( 'type' => 'warning', 'msg' => 'session_token' ); @@ -84,7 +84,7 @@ function session_check() { $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; unset($_SESSION["dual-login"]); - header("Location: /mailbox.php"); + header("Location: /mailbox"); exit(); } else { diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 892d79fb40..d9d8735134 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -6,7 +6,7 @@ unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_method']); - header("Location: /user.php"); + header("Location: /user"); } } @@ -17,19 +17,19 @@ $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "admin"; $_SESSION['mailcow_cc_last_login'] = last_login($login_user); - header("Location: /admin.php"); + header("Location: /admin"); } elseif ($as == "domainadmin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "domainadmin"; $_SESSION['mailcow_cc_last_login'] = last_login($login_user); - header("Location: /mailbox.php"); + header("Location: /mailbox"); } elseif ($as == "user") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "user"; $_SESSION['mailcow_cc_last_login'] = last_login($login_user); - header("Location: /user.php"); + header("Location: /user"); } elseif ($as != "pending") { unset($_SESSION['pending_mailcow_cc_username']); @@ -40,7 +40,7 @@ } } -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)) { @@ -49,7 +49,16 @@ $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; $_SESSION['mailcow_cc_username'] = $duallogin; $_SESSION['mailcow_cc_role'] = "user"; - header("Location: /user.php"); + header("Location: /user"); + } + } + else { + if (!empty(domain_admin('details', $duallogin))) { + $_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username']; + $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; + $_SESSION['mailcow_cc_username'] = $duallogin; + $_SESSION['mailcow_cc_role'] = "domainadmin"; + header("Location: /user"); } } } @@ -84,5 +93,8 @@ if (isset($_POST["rspamd_ui"])) { rspamd_ui('edit', $_POST); } + if (isset($_POST["mass_send"])) { + sys_mail($_POST); + } } ?> diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 4ac0df4793..9cb4c0dad9 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -9,6 +9,7 @@ // 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'); @@ -86,7 +87,12 @@ $DEFAULT_THEME = 'lumen'; // Password complexity as regular expression -$PASSWD_REGEP = '.{4,}'; +// Min. 6 characters +//$PASSWD_REGEP = '.{6,}'; +// Min. 6 characters, which must include at least one uppercase letter, one lowercase letter and one number +// $PASSWD_REGEP = '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{6,}$'; +// Min. 6 characters, which must include at least one letter and one number +$PASSWD_REGEP = '^(?=.*[0-9])(?=.*[A-Za-z]).{6,}$'; // Show DKIM private keys - false by default $SHOW_DKIM_PRIV_KEYS = false; @@ -122,3 +128,12 @@ // 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 47339923b9..fcb35de94f 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -1,19 +1,20 @@ @@ -107,6 +108,6 @@
- + ' + ' 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; @@ -125,11 +176,26 @@ jQuery(function($){ }); } else if (table == 'domainadminstable') { $.each(data, function (i, item) { - item.selected_domains = escapeHtml(item.selected_domains.toString().replace(/,/g, " ")); + item.selected_domains = escapeHtml(item.selected_domains); + item.selected_domains = item.selected_domains.toString().replace(/,/g, "
"); item.chkbox = ''; item.action = ''; + }); + } else if (table == 'adminstable') { + $.each(data, function (i, item) { + if (admin_username == item.username) { + item.usr = '→ ' + item.username; + } else { + item.usr = item.username; + } + item.chkbox = ''; + item.action = ''; }); } @@ -137,6 +203,7 @@ jQuery(function($){ }; // Initial table drawings draw_domain_admins(); + draw_admins(); draw_fwd_hosts(); draw_relayhosts(); // Relayhost diff --git a/data/web/js/api.js b/data/web/js/api.js index 3f9bc4b7b3..1a5f4b06bd 100644 --- a/data/web/js/api.js +++ b/data/web/js/api.js @@ -3,7 +3,11 @@ $(document).ready(function() { if ($(elem).data('submitted') == '1') { return true; } else { - $(elem).text(loading_text); + var parent_btn_grp = $(elem).parentsUntil(".btn-group").parent(); + if (parent_btn_grp.hasClass('btn-group')) { + parent_btn_grp.replaceWith(' + + +

+
+
+
+
+
+ + + + +
+
+ + @@ -304,17 +326,21 @@ - - + +
- +
- +
@@ -35,7 +35,7 @@
- +
@@ -56,18 +56,18 @@ + +