diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 11402129a..71cd7eda9 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: mailcow custom: ["https://www.servercow.de/mailcow?lang=en#sal"] diff --git a/data/Dockerfiles/acme/acme.sh b/data/Dockerfiles/acme/acme.sh index 3c7658d84..64a4d1765 100755 --- a/data/Dockerfiles/acme/acme.sh +++ b/data/Dockerfiles/acme/acme.sh @@ -4,9 +4,9 @@ exec 5>&1 # Do not attempt to write to slave if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then - export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}" + export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" else - export REDIS_CMDLINE="redis-cli -h redis -p 6379" + export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" fi until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do diff --git a/data/Dockerfiles/acme/obtain-certificate.sh b/data/Dockerfiles/acme/obtain-certificate.sh index 743441197..16c4e2588 100644 --- a/data/Dockerfiles/acme/obtain-certificate.sh +++ b/data/Dockerfiles/acme/obtain-certificate.sh @@ -124,7 +124,7 @@ case "$SUCCESS" in ;; *) # non-zero is non-fun log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'" - redis-cli -h redis SET ACME_FAIL_TIME "$(date +%s)" + redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)" exit 100${SUCCESS} ;; esac diff --git a/data/Dockerfiles/dockerapi/main.py b/data/Dockerfiles/dockerapi/main.py index 6f7a6042c..00c2ad5e3 100644 --- a/data/Dockerfiles/dockerapi/main.py +++ b/data/Dockerfiles/dockerapi/main.py @@ -34,9 +34,9 @@ async def lifespan(app: FastAPI): # Init redis client if os.environ['REDIS_SLAVEOF_IP'] != "": - redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0") + redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0", password=os.environ['REDISPASS']) else: - redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0") + redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS']) # Init docker clients sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') diff --git a/data/Dockerfiles/dovecot/clean_q_aged.sh b/data/Dockerfiles/dovecot/clean_q_aged.sh index ef6b61f1f..3fa8a7ddb 100755 --- a/data/Dockerfiles/dovecot/clean_q_aged.sh +++ b/data/Dockerfiles/dovecot/clean_q_aged.sh @@ -2,7 +2,7 @@ source /source_env.sh -MAX_AGE=$(redis-cli --raw -h redis-mailcow GET Q_MAX_AGE) +MAX_AGE=$(redis-cli --raw -h redis-mailcow -a ${REDISPASS} --no-auth-warning GET Q_MAX_AGE) if [[ -z ${MAX_AGE} ]]; then echo "Max age for quarantine items not defined" diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index c394bac5e..9d9490f6f 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -14,9 +14,9 @@ done # Do not attempt to write to slave if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then - REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}" + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" else - REDIS_CMDLINE="redis-cli -h redis -p 6379" + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" fi until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do diff --git a/data/Dockerfiles/dovecot/quarantine_notify.py b/data/Dockerfiles/dovecot/quarantine_notify.py index e8d743b31..dfcb1f2c6 100755 --- a/data/Dockerfiles/dovecot/quarantine_notify.py +++ b/data/Dockerfiles/dovecot/quarantine_notify.py @@ -31,7 +31,7 @@ try: while True: try: - r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0) + r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) r.ping() except Exception as ex: print('%s - trying again...' % (ex)) diff --git a/data/Dockerfiles/dovecot/quota_notify.py b/data/Dockerfiles/dovecot/quota_notify.py index 34b3e0ed9..c2c73e7a9 100755 --- a/data/Dockerfiles/dovecot/quota_notify.py +++ b/data/Dockerfiles/dovecot/quota_notify.py @@ -23,7 +23,7 @@ else: while True: try: - r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0) + r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) r.ping() except Exception as ex: print('%s - trying again...' % (ex)) diff --git a/data/Dockerfiles/dovecot/repl_health.sh b/data/Dockerfiles/dovecot/repl_health.sh index 93b66da49..2d7674bdc 100755 --- a/data/Dockerfiles/dovecot/repl_health.sh +++ b/data/Dockerfiles/dovecot/repl_health.sh @@ -4,9 +4,9 @@ source /source_env.sh # Do not attempt to write to slave if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then - REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}" + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" else - REDIS_CMDLINE="redis-cli -h redis -p 6379" + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" fi # Is replication active? diff --git a/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf b/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf index 519928954..4b9bf287c 100644 --- a/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf @@ -20,6 +20,7 @@ destination d_redis_ui_log { host("`REDIS_SLAVEOF_IP`") persist-name("redis1") port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") ); }; @@ -28,6 +29,7 @@ destination d_redis_f2b_channel { host("`REDIS_SLAVEOF_IP`") persist-name("redis2") port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") ); }; diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf index 3e929e7b9..c79eb92ee 100644 --- a/data/Dockerfiles/dovecot/syslog-ng.conf +++ b/data/Dockerfiles/dovecot/syslog-ng.conf @@ -20,6 +20,7 @@ destination d_redis_ui_log { host("redis-mailcow") persist-name("redis1") port(6379) + auth("`REDISPASS`") command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") ); }; @@ -28,6 +29,7 @@ destination d_redis_f2b_channel { host("redis-mailcow") persist-name("redis2") port(6379) + auth("`REDISPASS`") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") ); }; diff --git a/data/Dockerfiles/dovecot/trim_logs.sh b/data/Dockerfiles/dovecot/trim_logs.sh index 6b14c0c76..3b5e05278 100755 --- a/data/Dockerfiles/dovecot/trim_logs.sh +++ b/data/Dockerfiles/dovecot/trim_logs.sh @@ -10,9 +10,9 @@ catch_non_zero() { source /source_env.sh # Do not attempt to write to slave if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then - REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}" + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" else - REDIS_CMDLINE="redis-cli -h redis -p 6379" + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" fi catch_non_zero "${REDIS_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}" diff --git a/data/Dockerfiles/netfilter/main.py b/data/Dockerfiles/netfilter/main.py index c5667dc5f..36304bf0c 100644 --- a/data/Dockerfiles/netfilter/main.py +++ b/data/Dockerfiles/netfilter/main.py @@ -106,7 +106,7 @@ def get_ip(address): ip = ip.ipv4_mapped if ip.is_private or ip.is_loopback: return False - + return ip def ban(address): @@ -434,9 +434,9 @@ if __name__ == '__main__': redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '') redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '') if "".__eq__(redis_slaveof_ip): - r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0) + r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) else: - r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0) + r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS']) r.ping() pubsub = r.pubsub() except Exception as ex: @@ -452,7 +452,7 @@ if __name__ == '__main__': # clear bans in redis r.delete('F2B_ACTIVE_BANS') r.delete('F2B_PERM_BANS') - + refreshF2boptions() watch_thread = Thread(target=watch) diff --git a/data/Dockerfiles/nginx/Dockerfile b/data/Dockerfiles/nginx/Dockerfile new file mode 100644 index 000000000..7d2ce34f3 --- /dev/null +++ b/data/Dockerfiles/nginx/Dockerfile @@ -0,0 +1,18 @@ +FROM nginx:alpine +LABEL maintainer "The Infrastructure Company GmbH " + +ENV PIP_BREAK_SYSTEM_PACKAGES=1 + +RUN apk add --no-cache nginx \ + python3 \ + py3-pip && \ + pip install --upgrade pip && \ + pip install Jinja2 + +RUN mkdir -p /etc/nginx/includes + +COPY ./bootstrap.py / +COPY ./docker-entrypoint.sh / + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/data/Dockerfiles/nginx/bootstrap.py b/data/Dockerfiles/nginx/bootstrap.py new file mode 100644 index 000000000..0d24ae9b2 --- /dev/null +++ b/data/Dockerfiles/nginx/bootstrap.py @@ -0,0 +1,76 @@ +import os +import subprocess +from jinja2 import Environment, FileSystemLoader + + +def sites_default_conf(env, template_vars): + config_name = "sites-default.conf" + template = env.get_template(f"{config_name}.j2") + config = template.render(template_vars) + + with open(f"/etc/nginx/includes/{config_name}", "w") as f: + f.write(config) + +def nginx_conf(env, template_vars): + config_name = "nginx.conf" + template = env.get_template(f"{config_name}.j2") + config = template.render(template_vars) + + with open(f"/etc/nginx/{config_name}", "w") as f: + f.write(config) + +def prepare_template_vars(): + template_vars = { + 'IPV4_NETWORK': os.getenv("IPV4_NETWORK", "172.22.1"), + 'TRUSTED_NETWORK': os.getenv("TRUSTED_NETWORK", False), + 'SKIP_RSPAMD': os.getenv("SKIP_RSPAMD", "n").lower() in ("y", "yes"), + 'SKIP_SOGO': os.getenv("SKIP_SOGO", "n").lower() in ("y", "yes"), + 'NGINX_USE_PROXY_PROTOCOL': os.getenv("NGINX_USE_PROXY_PROTOCOL", "n").lower() in ("y", "yes"), + 'MAILCOW_HOSTNAME': os.getenv("MAILCOW_HOSTNAME", ""), + 'ADDITIONAL_SERVER_NAMES': os.getenv("ADDITIONAL_SERVER_NAMES", "").replace(',', ' '), + 'HTTP_PORT': os.getenv("HTTP_PORT", "80"), + 'HTTPS_PORT': os.getenv("HTTPS_PORT", "443"), + 'SOGOHOST': os.getenv("SOGOHOST", "sogo-mailcow"), + 'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"), + 'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"), + } + + ssl_dir = '/etc/ssl/mail/' + template_vars['valid_cert_dirs'] = [] + for d in os.listdir(ssl_dir): + full_path = os.path.join(ssl_dir, d) + if not os.path.isdir(full_path): + continue + + cert_path = os.path.join(full_path, 'cert.pem') + key_path = os.path.join(full_path, 'key.pem') + domains_path = os.path.join(full_path, 'domains') + + if os.path.isfile(cert_path) and os.path.isfile(key_path) and os.path.isfile(domains_path): + with open(domains_path, 'r') as file: + domains = file.read().strip() + domains_list = domains.split() + if domains_list and template_vars["MAILCOW_HOSTNAME"] not in domains_list: + template_vars['valid_cert_dirs'].append({ + 'cert_path': full_path + '/', + 'domains': domains + }) + + return template_vars + +def main(): + env = Environment(loader=FileSystemLoader('./etc/nginx/conf.d')) + + # Render config + print("Render config") + template_vars = prepare_template_vars() + sites_default_conf(env, template_vars) + nginx_conf(env, template_vars) + + # Validate config + print("Validate config") + subprocess.run(["nginx", "-qt"]) + + +if __name__ == "__main__": + main() diff --git a/data/Dockerfiles/nginx/docker-entrypoint.sh b/data/Dockerfiles/nginx/docker-entrypoint.sh new file mode 100755 index 000000000..beb0b4d9e --- /dev/null +++ b/data/Dockerfiles/nginx/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +until ping ${REDISHOST} -c1 > /dev/null; do + echo "Waiting for Redis..." + sleep 1 +done +until ping ${PHPFPMHOST} -c1 > /dev/null; do + echo "Waiting for PHP..." + sleep 1 +done +if printf "%s\n" "${SKIP_SOGO}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then + until ping ${SOGOHOST} -c1 > /dev/null; do + echo "Waiting for SOGo..." + sleep 1 + done +fi +if printf "%s\n" "${SKIP_RSPAMD}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then + until ping ${RSPAMDHOST} -c1 > /dev/null; do + echo "Waiting for Rspamd..." + sleep 1 + done +fi + +python3 /bootstrap.py + +exec "$@" diff --git a/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/data/Dockerfiles/phpfpm/docker-entrypoint.sh index 20e9a405c..e6510de7a 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -16,7 +16,7 @@ else REDIS_HOST="redis" REDIS_PORT="6379" fi -REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT}" +REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning" until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do echo "Waiting for Redis..." @@ -26,7 +26,7 @@ done # Set redis session store echo -n ' session.save_handler = redis -session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'" +session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'" ' > /usr/local/etc/php/conf.d/session_store.ini # Check mysql_upgrade (master and slave) diff --git a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf index cb1d1aa04..8e15932a2 100644 --- a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf @@ -20,6 +20,7 @@ destination d_redis_ui_log { host("`REDIS_SLAVEOF_IP`") persist-name("redis1") port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") ); }; @@ -28,6 +29,7 @@ destination d_redis_f2b_channel { host("`REDIS_SLAVEOF_IP`") persist-name("redis2") port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") ); }; diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index 0990f1c05..fc7d1aa0f 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -20,6 +20,7 @@ destination d_redis_ui_log { host("redis-mailcow") persist-name("redis1") port(6379) + auth("`REDISPASS`") command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") ); }; @@ -28,6 +29,7 @@ destination d_redis_f2b_channel { host("redis-mailcow") persist-name("redis2") port(6379) + auth("`REDISPASS`") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") ); }; diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index cf09ee48f..cf44c3063 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -56,27 +56,29 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then cat < /etc/rspamd/local.d/redis.conf read_servers = "redis:6379"; write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}"; +password = "${REDISPASS}"; timeout = 10; EOF - until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do + until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do echo "Waiting for Redis @redis-mailcow..." sleep 2 done - until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} PING) == "PONG" ]]; do + until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..." sleep 2 done - redis-cli -h redis-mailcow SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT} + redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT} else cat < /etc/rspamd/local.d/redis.conf servers = "redis:6379"; +password = "${REDISPASS}"; timeout = 10; EOF - until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do + until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do echo "Waiting for Redis slave..." sleep 2 done - redis-cli -h redis-mailcow SLAVEOF NO ONE + redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE fi # Provide additional lua modules diff --git a/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf b/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf index 7abfc4b59..675e4c67a 100644 --- a/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf @@ -22,6 +22,7 @@ destination d_redis_ui_log { host("`REDIS_SLAVEOF_IP`") persist-name("redis1") port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") ); }; @@ -30,6 +31,7 @@ destination d_redis_f2b_channel { host("`REDIS_SLAVEOF_IP`") persist-name("redis2") port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") ); }; diff --git a/data/Dockerfiles/sogo/syslog-ng.conf b/data/Dockerfiles/sogo/syslog-ng.conf index f16a2920a..8460f2f95 100644 --- a/data/Dockerfiles/sogo/syslog-ng.conf +++ b/data/Dockerfiles/sogo/syslog-ng.conf @@ -22,6 +22,7 @@ destination d_redis_ui_log { host("redis-mailcow") persist-name("redis1") port(6379) + auth("`REDISPASS`") command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") ); }; @@ -30,6 +31,7 @@ destination d_redis_f2b_channel { host("redis-mailcow") persist-name("redis2") port(6379) + auth("`REDISPASS`") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") ); }; diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index 81d65d907..dac0335fb 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -40,9 +40,9 @@ done # Do not attempt to write to slave if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then - REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}" + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" else - REDIS_CMDLINE="redis-cli -h redis -p 6379" + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" fi until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do @@ -330,7 +330,7 @@ redis_checks() { touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow host_ip=$(get_container_ip redis-mailcow) err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "PING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "AUTH ${REDISPASS}\nPING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 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 "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} @@ -503,12 +503,12 @@ dovecot_repl_checks() { err_count=0 diff_c=0 THRESHOLD=${DOVECOT_REPL_THRESHOLD} - D_REPL_STATUS=$(redis-cli -h redis -r GET DOVECOT_REPL_HEALTH) + D_REPL_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH) # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do err_c_cur=${err_count} - D_REPL_STATUS=$(redis-cli --raw -h redis GET DOVECOT_REPL_HEALTH) + D_REPL_STATUS=$(redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH) if [[ "${D_REPL_STATUS}" != "1" ]]; then err_count=$(( ${err_count} + 1 )) fi @@ -578,19 +578,19 @@ ratelimit_checks() { err_count=0 diff_c=0 THRESHOLD=${RATELIMIT_THRESHOLD} - RL_LOG_STATUS=$(redis-cli -h redis LRANGE RL_LOG 0 0 | jq .qid) + RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do err_c_cur=${err_count} RL_LOG_STATUS_PREV=${RL_LOG_STATUS} - RL_LOG_STATUS=$(redis-cli -h redis LRANGE RL_LOG 0 0 | jq .qid) + RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then err_count=$(( ${err_count} + 1 )) echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit echo 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit echo >> /tmp/ratelimit - redis-cli --raw -h redis LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit + redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit fi [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) @@ -673,7 +673,7 @@ acme_checks() { err_count=0 diff_c=0 THRESHOLD=${ACME_THRESHOLD} - ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME) + ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME) if [[ -z "${ACME_LOG_STATUS}" ]]; then ${REDIS_CMDLINE} SET ACME_FAIL_TIME 0 ACME_LOG_STATUS=0 @@ -685,7 +685,7 @@ acme_checks() { ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS} ACME_LC=0 until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do - ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME 2> /dev/null) + ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null) sleep 3 ACME_LC=$((ACME_LC+1)) done diff --git a/data/conf/mysql/my.cnf b/data/conf/mysql/my.cnf index b4c348863..489b973c4 100644 --- a/data/conf/mysql/my.cnf +++ b/data/conf/mysql/my.cnf @@ -20,7 +20,7 @@ thread_cache_size = 8 query_cache_type = 0 query_cache_size = 0 max_heap_table_size = 48M -thread_stack = 128K +thread_stack = 192K skip-host-cache skip-name-resolve log-warnings = 0 diff --git a/data/conf/nginx/000-map-size.conf b/data/conf/nginx/000-map-size.conf deleted file mode 100644 index a834306ed..000000000 --- a/data/conf/nginx/000-map-size.conf +++ /dev/null @@ -1,3 +0,0 @@ -map_hash_max_size 256; -map_hash_bucket_size 256; - diff --git a/data/conf/nginx/dynmaps.conf b/data/conf/nginx/dynmaps.conf deleted file mode 100644 index 99c0c6aaa..000000000 --- a/data/conf/nginx/dynmaps.conf +++ /dev/null @@ -1,19 +0,0 @@ -server { - listen 8081; - listen [::]:8081; - index index.php index.html; - server_name _; - error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; - root /dynmaps; - - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9001; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - } -} diff --git a/data/conf/nginx/includes/site-defaults.conf b/data/conf/nginx/includes/site-defaults.conf deleted file mode 100644 index 1d03e9398..000000000 --- a/data/conf/nginx/includes/site-defaults.conf +++ /dev/null @@ -1,242 +0,0 @@ - - include /etc/nginx/mime.types; - charset utf-8; - override_charset on; - - server_tokens off; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; - ssl_ecdh_curve X25519:X448:secp384r1:secp256k1; - ssl_session_cache shared:SSL:50m; - ssl_session_timeout 1d; - ssl_session_tickets off; - - add_header Strict-Transport-Security "max-age=15768000;"; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Robots-Tag none; - add_header X-Download-Options noopen; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Permitted-Cross-Domain-Policies none; - add_header Referrer-Policy strict-origin; - - index index.php index.html; - - client_max_body_size 0; - - gzip on; - gzip_disable "msie6"; - - gzip_vary on; - gzip_proxied off; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_min_length 256; - gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; - - location ~ ^/(fonts|js|css|img)/ { - expires max; - add_header Cache-Control public; - } - - error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; - fastcgi_hide_header X-Powered-By; - absolute_redirect off; - root /web; - - location / { - try_files $uri $uri/ @strip-ext; - } - - location /qhandler { - rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2; - } - - location /edit { - rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2; - } - - location @strip-ext { - rewrite ^(.*)$ $1.php last; - } - - location ~ ^/api/v1/(.*)$ { - try_files $uri $uri/ /json_api.php?query=$1&$args; - } - - location ^~ /.well-known/acme-challenge/ { - allow all; - default_type "text/plain"; - } - - # If behind reverse proxy, forwards the correct IP - set_real_ip_from 10.0.0.0/8; - set_real_ip_from 172.16.0.0/12; - set_real_ip_from 192.168.0.0/16; - set_real_ip_from fc00::/7; - real_ip_header X-Forwarded-For; - real_ip_recursive on; - - rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; - rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; - - location ^~ /principals { - return 301 /SOGo/dav; - } - - location ^~ /inc/lib/ { - deny all; - return 403; - } - - location ~ \.php$ { - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - fastcgi_index index.php; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_read_timeout 3600; - fastcgi_send_timeout 3600; - } - - location /rspamd/ { - location /rspamd/auth { - # proxy_pass is not inherited - proxy_pass http://rspamd:11334/auth; - proxy_intercept_errors on; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_redirect off; - error_page 401 /_rspamderror.php; - } - proxy_pass http://rspamd:11334/; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_redirect off; - } - - location ~* ^/Autodiscover/Autodiscover.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - try_files /autodiscover.php =404; - } - - location ~* ^/Autodiscover/Autodiscover.json { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - try_files /autodiscover-json.php =404; - } - - location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9002; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - try_files /autoconfig.php =404; - } - - location /sogo-auth-verify { - internal; - proxy_set_header X-Original-URI $request_uri; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $http_host; - proxy_set_header Content-Length ""; - proxy_pass http://127.0.0.1:65510/sogo-auth; - proxy_pass_request_body off; - } - - location ^~ /Microsoft-Server-ActiveSync { - include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; - include /etc/nginx/conf.d/sogo_eas.active; - proxy_connect_timeout 75; - proxy_send_timeout 3600; - proxy_read_timeout 3600; - proxy_buffer_size 128k; - proxy_buffers 64 512k; - proxy_busy_buffers_size 512k; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - client_body_buffer_size 512k; - client_max_body_size 0; - } - - location ^~ /SOGo { - location ~* ^/SOGo/so/.*\.(xml|js|html|xhtml)$ { - include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; - include /etc/nginx/conf.d/sogo.active; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header x-webobjects-server-protocol HTTP/1.0; - proxy_set_header x-webobjects-remote-host $remote_addr; - proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; - proxy_set_header x-webobjects-server-port $server_port; - proxy_hide_header Content-Type; - add_header Content-Type text/plain; - break; - } - include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf; - include /etc/nginx/conf.d/sogo.active; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header x-webobjects-server-protocol HTTP/1.0; - proxy_set_header x-webobjects-remote-host $remote_addr; - proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; - proxy_set_header x-webobjects-server-port $server_port; - proxy_buffer_size 128k; - proxy_buffers 64 512k; - proxy_busy_buffers_size 512k; - proxy_send_timeout 3600; - proxy_read_timeout 3600; - client_body_buffer_size 128k; - client_max_body_size 0; - break; - } - - location ~* /sogo$ { - return 301 $client_req_scheme://$http_host/SOGo; - } - - location /SOGo.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; - } - - location /.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; - } - - location /SOGo/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; - } - - location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { - alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; - } - - include /etc/nginx/conf.d/site.*.custom; - - error_page 502 @awaitingupstream; - - location @awaitingupstream { - rewrite ^(.*)$ /_status.502.html break; - } - - location ~ ^/cache/(.*)$ { - try_files $uri $uri/ /resource.php?file=$1; - } diff --git a/data/conf/nginx/includes/sogo_proxy_auth.conf b/data/conf/nginx/includes/sogo_proxy_auth.conf deleted file mode 100644 index 045b98adc..000000000 --- a/data/conf/nginx/includes/sogo_proxy_auth.conf +++ /dev/null @@ -1,8 +0,0 @@ -auth_request /sogo-auth-verify; -auth_request_set $user $upstream_http_x_user; -auth_request_set $auth $upstream_http_x_auth; -auth_request_set $auth_type $upstream_http_x_auth_type; -proxy_set_header x-webobjects-remote-user "$user"; -proxy_set_header Authorization "$auth"; -proxy_set_header x-webobjects-auth-type "$auth_type"; - diff --git a/data/conf/nginx/mailcow_auth.conf b/data/conf/nginx/mailcow_auth.conf deleted file mode 100644 index 3e4a8c736..000000000 --- a/data/conf/nginx/mailcow_auth.conf +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 9082 ssl http2; - - ssl_certificate /etc/ssl/mail/cert.pem; - ssl_certificate_key /etc/ssl/mail/key.pem; - - index mailcowauth.php; - server_name _; - error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; - root /mailcowauth; - client_max_body_size 10M; - - location ~ \.php$ { - client_max_body_size 10M; - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9001; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - } -} diff --git a/data/conf/nginx/meta_exporter.conf b/data/conf/nginx/meta_exporter.conf deleted file mode 100644 index 74291b147..000000000 --- a/data/conf/nginx/meta_exporter.conf +++ /dev/null @@ -1,19 +0,0 @@ -server { - listen 9081; - index index.php index.html; - server_name _; - error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; - root /meta_exporter; - client_max_body_size 10M; - location ~ \.php$ { - client_max_body_size 10M; - try_files $uri =404; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9001; - fastcgi_index pipe.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - } -} diff --git a/data/conf/nginx/nginx.conf.j2 b/data/conf/nginx/nginx.conf.j2 new file mode 100644 index 000000000..833c480fd --- /dev/null +++ b/data/conf/nginx/nginx.conf.j2 @@ -0,0 +1,142 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + # map-size.conf: + map_hash_max_size 256; + map_hash_bucket_size 256; + + # site.conf: + proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; + server_names_hash_max_size 512; + server_names_hash_bucket_size 128; + + map $http_x_forwarded_proto $client_req_scheme { + default $scheme; + https https; + } + + # Default + server { + listen 127.0.0.1:65510; # sogo-auth verify internal + listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + http2 on; + + ssl_certificate /etc/ssl/mail/cert.pem; + ssl_certificate_key /etc/ssl/mail/key.pem; + + server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES }}; + + include /etc/nginx/includes/sites-default.conf; + } + + # rspamd dynmaps: + server { + listen 8081; + listen [::]:8081; + index index.php index.html; + server_name _; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /dynmaps; + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9001; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } + + # rspamd meta_exporter: + server { + listen 9081; + index index.php index.html; + server_name _; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /meta_exporter; + client_max_body_size 10M; + location ~ \.php$ { + client_max_body_size 10M; + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9001; + fastcgi_index pipe.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } + + server { + listen 9082 ssl http2; + + ssl_certificate /etc/ssl/mail/cert.pem; + ssl_certificate_key /etc/ssl/mail/key.pem; + + index mailcowauth.php; + server_name _; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /mailcowauth; + client_max_body_size 10M; + location ~ \.php$ { + client_max_body_size 10M; + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9001; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } + + {% for cert in valid_cert_dirs %} + server { + listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + http2 on; + + ssl_certificate {{ cert.cert_path }}cert.pem; + ssl_certificate_key {{ cert.cert_path }}key.pem; + + server_name {{ cert.domains }}; + + include /etc/nginx/includes/sites-default.conf; + } + {% endfor %} +} diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf deleted file mode 100644 index fb40de879..000000000 --- a/data/conf/nginx/site.conf +++ /dev/null @@ -1,10 +0,0 @@ -proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; -server_names_hash_max_size 512; -server_names_hash_bucket_size 128; - -map $http_x_forwarded_proto $client_req_scheme { - default $scheme; - https https; -} - -include /etc/nginx/conf.d/sites.active; diff --git a/data/conf/nginx/sites-default.conf.j2 b/data/conf/nginx/sites-default.conf.j2 new file mode 100644 index 000000000..783723bfc --- /dev/null +++ b/data/conf/nginx/sites-default.conf.j2 @@ -0,0 +1,276 @@ +include /etc/nginx/mime.types; +charset utf-8; +override_charset on; + +server_tokens off; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers on; +ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; +ssl_ecdh_curve X25519:X448:secp384r1:secp256k1; +ssl_session_cache shared:SSL:50m; +ssl_session_timeout 1d; +ssl_session_tickets off; + +add_header Strict-Transport-Security "max-age=15768000;"; +add_header X-Content-Type-Options nosniff; +add_header X-XSS-Protection "1; mode=block"; +add_header X-Robots-Tag none; +add_header X-Download-Options noopen; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Permitted-Cross-Domain-Policies none; +add_header Referrer-Policy strict-origin; + +index index.php index.html; + +client_max_body_size 0; + +gzip on; +gzip_disable "msie6"; + +gzip_vary on; +gzip_proxied off; +gzip_comp_level 6; +gzip_buffers 16 8k; +gzip_http_version 1.1; +gzip_min_length 256; +gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; + +location ~ ^/(fonts|js|css|img)/ { + expires max; + add_header Cache-Control public; +} + +error_log /var/log/nginx/error.log; +access_log /var/log/nginx/access.log; +fastcgi_hide_header X-Powered-By; +absolute_redirect off; +root /web; + +# If behind reverse proxy, forwards the correct IP +set_real_ip_from 10.0.0.0/8; +set_real_ip_from 172.16.0.0/12; +set_real_ip_from 192.168.0.0/16; +set_real_ip_from fc00::/7; +{% if not TRUSTED_NETWORK %} +real_ip_header X-Forwarded-For; +{% else %} +set_real_ip_from {{ TRUSTED_NETWORK }}; +real_ip_header proxy_protocol; +{% endif %} +real_ip_recursive on; + + +location @strip-ext { + rewrite ^(.*)$ $1.php last; +} + +location ^~ /inc/lib/ { + deny all; + return 403; +} + +location ^~ /.well-known/acme-challenge/ { + allow all; + default_type "text/plain"; +} + +rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; +rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; + + +location / { + try_files $uri $uri/ @strip-ext; +} + +location /qhandler { + rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2; +} + +location /edit { + rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2; +} + +location ~ ^/api/v1/(.*)$ { + try_files $uri $uri/ /json_api.php?query=$1&$args; +} + +location ~ ^/cache/(.*)$ { + try_files $uri $uri/ /resource.php?file=$1; +} + +location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + fastcgi_index index.php; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_read_timeout 3600; + fastcgi_send_timeout 3600; +} + +location ~* ^/Autodiscover/Autodiscover.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover.php =404; +} + +location ~* ^/Autodiscover/Autodiscover.json { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover-json.php =404; +} + +location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autoconfig.php =404; +} + +{% if not SKIP_RSPAMD %} +location /rspamd/ { + proxy_pass http://{{ RSPAMDHOST }}:11334/; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_redirect off; + proxy_intercept_errors on; + error_page 401 /_rspamderror.php; +} +{% endif %} + +{% if not SKIP_SOGO %} +location ^~ /principals { + return 301 /SOGo/dav; +} + +location /sogo-auth-verify { + internal; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_set_header Content-Length ""; + proxy_pass http://127.0.0.1:65510/sogo-auth; + proxy_pass_request_body off; +} + +location ^~ /Microsoft-Server-ActiveSync { + auth_request /sogo-auth-verify; + auth_request_set $user $upstream_http_x_user; + auth_request_set $auth $upstream_http_x_auth; + auth_request_set $auth_type $upstream_http_x_auth_type; + proxy_set_header x-webobjects-remote-user "$user"; + proxy_set_header Authorization "$auth"; + proxy_set_header x-webobjects-auth-type "$auth_type"; + + proxy_pass http://{{ SOGOHOST }}:20000/SOGo/Microsoft-Server-ActiveSync; + + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_connect_timeout 75; + proxy_send_timeout 3600; + proxy_read_timeout 3600; + proxy_buffer_size 128k; + proxy_buffers 64 512k; + proxy_busy_buffers_size 512k; + proxy_set_header Host $http_host; + client_body_buffer_size 512k; + client_max_body_size 0; +} + +location ^~ /SOGo { + location ~* ^/SOGo/so/.*\.(xml|js|html|xhtml)$ { + auth_request /sogo-auth-verify; + auth_request_set $user $upstream_http_x_user; + auth_request_set $auth $upstream_http_x_auth; + auth_request_set $auth_type $upstream_http_x_auth_type; + proxy_set_header x-webobjects-remote-user "$user"; + proxy_set_header Authorization "$auth"; + proxy_set_header x-webobjects-auth-type "$auth_type"; + + proxy_pass http://{{ SOGOHOST }}:20000; + + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header Host $http_host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; + proxy_set_header x-webobjects-server-port $server_port; + proxy_hide_header Content-Type; + add_header Content-Type text/plain; + break; + } + auth_request /sogo-auth-verify; + auth_request_set $user $upstream_http_x_user; + auth_request_set $auth $upstream_http_x_auth; + auth_request_set $auth_type $upstream_http_x_auth_type; + proxy_set_header x-webobjects-remote-user "$user"; + proxy_set_header Authorization "$auth"; + proxy_set_header x-webobjects-auth-type "$auth_type"; + + proxy_pass http://{{ SOGOHOST }}:20000; + + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header Host $http_host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; + proxy_set_header x-webobjects-server-port $server_port; + proxy_buffer_size 128k; + proxy_buffers 64 512k; + proxy_busy_buffers_size 512k; + proxy_send_timeout 3600; + proxy_read_timeout 3600; + client_body_buffer_size 128k; + client_max_body_size 0; + break; +} + +location ~* /sogo$ { + return 301 $client_req_scheme://$http_host/SOGo; +} + +location /SOGo.woa/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; +} + +location /.woa/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; +} + +location /SOGo/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; +} + +location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { + alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; +} +{% endif %} + + +include /etc/nginx/conf.d/site.*.custom; + +error_page 502 @awaitingupstream; + +location @awaitingupstream { + rewrite ^(.*)$ /_status.502.html break; +} + +location ~* \.php$ { + return 404; +} +location ~* \.twig$ { + return 404; +} diff --git a/data/conf/nginx/templates/listen_plain.template b/data/conf/nginx/templates/listen_plain.template deleted file mode 100644 index a044b22f2..000000000 --- a/data/conf/nginx/templates/listen_plain.template +++ /dev/null @@ -1,2 +0,0 @@ -listen ${HTTP_PORT}; -listen [::]:${HTTP_PORT}; diff --git a/data/conf/nginx/templates/listen_ssl.template b/data/conf/nginx/templates/listen_ssl.template deleted file mode 100644 index 40c402d04..000000000 --- a/data/conf/nginx/templates/listen_ssl.template +++ /dev/null @@ -1,3 +0,0 @@ -listen ${HTTPS_PORT} ssl; -listen [::]:${HTTPS_PORT} ssl; -http2 on; diff --git a/data/conf/nginx/templates/server_name.template.sh b/data/conf/nginx/templates/server_name.template.sh deleted file mode 100755 index 291b378fb..000000000 --- a/data/conf/nginx/templates/server_name.template.sh +++ /dev/null @@ -1 +0,0 @@ -echo "server_name ${MAILCOW_HOSTNAME} autodiscover.* autoconfig.* $(echo ${ADDITIONAL_SERVER_NAMES} | tr ',' ' ');" diff --git a/data/conf/nginx/templates/sites.template.sh b/data/conf/nginx/templates/sites.template.sh deleted file mode 100644 index 782c8141b..000000000 --- a/data/conf/nginx/templates/sites.template.sh +++ /dev/null @@ -1,38 +0,0 @@ -echo ' -server { - listen 127.0.0.1:65510; - include /etc/nginx/conf.d/listen_plain.active; - include /etc/nginx/conf.d/listen_ssl.active; - - ssl_certificate /etc/ssl/mail/cert.pem; - ssl_certificate_key /etc/ssl/mail/key.pem; - - include /etc/nginx/conf.d/server_name.active; - - include /etc/nginx/conf.d/includes/site-defaults.conf; -} -'; -for cert_dir in /etc/ssl/mail/*/ ; do - if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then - continue - fi - # do not create vhost for default-certificate. the cert is already in the default server listen - domains="$(cat ${cert_dir}domains | sed -e 's/^[[:space:]]*//')" - case "${domains}" in - "") continue;; - "${MAILCOW_HOSTNAME}"*) continue;; - esac - echo -n ' -server { - include /etc/nginx/conf.d/listen_ssl.active; - - ssl_certificate '${cert_dir}'cert.pem; - ssl_certificate_key '${cert_dir}'key.pem; -'; - echo -n ' - server_name '${domains}'; - - include /etc/nginx/conf.d/includes/site-defaults.conf; -} -'; -done diff --git a/data/conf/nginx/templates/sogo.template b/data/conf/nginx/templates/sogo.template deleted file mode 100644 index 2c084389f..000000000 --- a/data/conf/nginx/templates/sogo.template +++ /dev/null @@ -1 +0,0 @@ -proxy_pass http://${IPV4_NETWORK}.248:20000; diff --git a/data/conf/nginx/templates/sogo_eas.template.sh b/data/conf/nginx/templates/sogo_eas.template.sh deleted file mode 100644 index b241ef0e7..000000000 --- a/data/conf/nginx/templates/sogo_eas.template.sh +++ /dev/null @@ -1,5 +0,0 @@ -if printf "%s\n" "${SKIP_SOGO}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then - echo "return 410;" -else - echo "proxy_pass http://${IPV4_NETWORK}.248:20000/SOGo/Microsoft-Server-ActiveSync;" -fi diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index 7a1cbf6ae..1dbe94773 100644 --- a/data/conf/postfix/postscreen_access.cidr +++ b/data/conf/postfix/postscreen_access.cidr @@ -1,6 +1,6 @@ -# Whitelist generated by Postwhite v3.4 on Fri Nov 1 00:18:49 UTC 2024 +# Whitelist generated by Postwhite v3.4 on Sun Dec 1 00:21:36 UTC 2024 # https://github.com/stevejenkins/postwhite/ -# 2013 total rules +# 1971 total rules 2a00:1450:4000::/36 permit 2a01:111:f400::/48 permit 2a01:111:f403:8000::/50 permit @@ -19,8 +19,7 @@ 8.20.114.31 permit 8.25.194.0/23 permit 8.25.196.0/23 permit -8.39.54.0/23 permit -8.40.222.0/23 permit +10.162.0.0/16 permit 12.130.86.238 permit 13.110.208.0/21 permit 13.110.209.0/24 permit @@ -31,9 +30,11 @@ 15.200.21.50 permit 15.200.44.248 permit 15.200.201.185 permit +17.41.0.0/16 permit 17.57.155.0/24 permit 17.57.156.0/24 permit 17.58.0.0/16 permit +17.142.0.0/15 permit 17.143.234.140/30 permit 18.156.89.250 permit 18.157.243.190 permit @@ -116,7 +117,6 @@ 40.233.64.216 permit 40.233.83.78 permit 40.233.88.28 permit -43.228.184.0/22 permit 44.206.138.57 permit 44.217.45.156 permit 44.236.56.93 permit @@ -325,7 +325,6 @@ 65.110.161.77 permit 65.123.29.213 permit 65.123.29.220 permit -65.154.166.0/24 permit 65.212.180.36 permit 66.102.0.0/20 permit 66.119.150.192/26 permit @@ -1114,10 +1113,8 @@ 98.139.245.212/31 permit 99.78.197.208/28 permit 99.83.190.102 permit -103.2.140.0/22 permit 103.9.96.0/22 permit 103.28.42.0/24 permit -103.47.204.0/22 permit 103.151.192.0/23 permit 103.168.172.128/27 permit 104.43.243.237 permit @@ -1285,9 +1282,6 @@ 117.120.16.0/21 permit 119.42.242.52/31 permit 119.42.242.156 permit -121.244.91.48 permit -121.244.91.52 permit -122.15.156.182 permit 123.126.78.64/29 permit 124.108.96.24/31 permit 124.108.96.28/31 permit @@ -1348,19 +1342,7 @@ 134.170.141.64/26 permit 134.170.143.0/24 permit 134.170.174.0/24 permit -135.84.80.0/24 permit -135.84.81.0/24 permit -135.84.82.0/24 permit -135.84.83.0/24 permit 135.84.216.0/22 permit -136.143.160.0/24 permit -136.143.161.0/24 permit -136.143.162.0/24 permit -136.143.178.49 permit -136.143.182.0/23 permit -136.143.184.0/24 permit -136.143.188.0/24 permit -136.143.190.0/23 permit 136.147.128.0/20 permit 136.147.135.0/24 permit 136.147.176.0/20 permit @@ -1375,7 +1357,6 @@ 139.138.46.219 permit 139.138.57.55 permit 139.138.58.119 permit -139.167.79.86 permit 139.180.17.0/24 permit 140.238.148.191 permit 141.148.159.229 permit @@ -1410,6 +1391,7 @@ 146.20.215.0/24 permit 146.20.215.182 permit 146.88.28.0/24 permit +147.154.32.0/25 permit 147.243.1.47 permit 147.243.1.48 permit 147.243.1.153 permit @@ -1450,7 +1432,6 @@ 157.151.208.65 permit 157.255.1.64/29 permit 158.101.211.207 permit -158.120.80.0/21 permit 158.247.16.0/20 permit 159.92.154.0/24 permit 159.92.155.0/24 permit @@ -1478,6 +1459,11 @@ 161.38.204.0/22 permit 161.71.32.0/19 permit 161.71.64.0/20 permit +162.88.4.0/23 permit +162.88.8.0/24 permit +162.88.24.0/24 permit +162.88.25.0/24 permit +162.88.36.0/24 permit 162.247.216.0/22 permit 163.47.180.0/22 permit 163.114.130.16 permit @@ -1486,7 +1472,6 @@ 163.114.135.16 permit 164.152.23.32 permit 164.177.132.168/30 permit -165.173.128.0/24 permit 166.78.68.0/22 permit 166.78.68.221 permit 166.78.69.169 permit @@ -1515,12 +1500,6 @@ 168.245.12.252 permit 168.245.46.9 permit 168.245.127.231 permit -169.148.129.0/24 permit -169.148.131.0/24 permit -169.148.142.10 permit -169.148.144.0/25 permit -169.148.144.10 permit -170.10.68.0/22 permit 170.10.128.0/24 permit 170.10.129.0/24 permit 170.10.132.56/29 permit @@ -1626,6 +1605,7 @@ 192.18.139.154 permit 192.18.145.36 permit 192.18.152.58 permit +192.29.103.128/25 permit 192.30.252.0/22 permit 192.161.144.0/20 permit 192.162.87.0/24 permit @@ -1651,14 +1631,6 @@ 195.234.109.226 permit 195.245.230.0/23 permit 198.2.128.0/18 permit -198.2.128.0/24 permit -198.2.132.0/22 permit -198.2.136.0/23 permit -198.2.145.0/24 permit -198.2.177.0/24 permit -198.2.178.0/23 permit -198.2.180.0/24 permit -198.2.186.0/23 permit 198.21.0.0/21 permit 198.37.144.0/20 permit 198.37.152.186 permit @@ -1678,15 +1650,7 @@ 199.16.156.0/22 permit 199.33.145.1 permit 199.33.145.32 permit -199.34.22.36 permit 199.59.148.0/22 permit -199.67.80.2 permit -199.67.80.20 permit -199.67.82.2 permit -199.67.82.20 permit -199.67.84.0/24 permit -199.67.86.0/24 permit -199.67.88.0/24 permit 199.101.161.130 permit 199.101.162.0/25 permit 199.122.120.0/21 permit @@ -1698,7 +1662,6 @@ 202.165.102.47 permit 202.177.148.100 permit 202.177.148.110 permit -203.31.36.0/22 permit 203.32.4.25 permit 203.55.21.0/24 permit 203.81.17.0/24 permit @@ -1744,19 +1707,13 @@ 204.92.114.187 permit 204.92.114.203 permit 204.92.114.204/31 permit -204.141.32.0/23 permit -204.141.42.0/23 permit 204.220.160.0/21 permit 204.220.168.0/21 permit 204.220.176.0/20 permit 204.232.168.0/24 permit 205.139.110.0/24 permit 205.201.128.0/20 permit -205.201.131.128/25 permit -205.201.134.128/25 permit -205.201.136.0/23 permit 205.201.137.229 permit -205.201.139.0/24 permit 205.207.104.0/22 permit 205.220.167.17 permit 205.220.167.98 permit @@ -1784,7 +1741,6 @@ 207.46.132.128/27 permit 207.46.198.0/25 permit 207.46.200.0/27 permit -207.58.147.64/28 permit 207.67.38.0/24 permit 207.67.98.192/27 permit 207.68.176.0/26 permit @@ -1831,6 +1787,8 @@ 208.74.204.5 permit 208.74.204.9 permit 208.75.120.0/22 permit +208.76.62.0/24 permit +208.76.63.0/24 permit 208.82.237.96/29 permit 208.82.237.104/31 permit 208.82.238.96/29 permit @@ -1930,7 +1888,6 @@ 213.199.177.0/26 permit 216.17.150.242 permit 216.17.150.251 permit -216.22.15.224/27 permit 216.24.224.0/20 permit 216.39.60.154/31 permit 216.39.60.156/30 permit @@ -1973,7 +1930,10 @@ 216.136.162.65 permit 216.136.162.120/29 permit 216.136.168.80/28 permit +216.139.64.0/19 permit 216.145.221.0/24 permit +216.146.32.0/24 permit +216.146.33.0/24 permit 216.198.0.0/18 permit 216.203.30.55 permit 216.203.33.178/31 permit @@ -1999,8 +1959,6 @@ 2603:1030:20e:3::23c permit 2603:1030:b:3::152 permit 2603:1030:c02:8::14 permit -2607:13c0:0001:0000:0000:0000:0000:7000/116 permit -2607:13c0:0002:0000:0000:0000:0000:1000/116 permit 2607:f8b0:4000::/36 permit 2620:109:c003:104::/64 permit 2620:109:c003:104::215 permit diff --git a/data/conf/redis/redis-conf.sh b/data/conf/redis/redis-conf.sh new file mode 100755 index 000000000..95d50a39a --- /dev/null +++ b/data/conf/redis/redis-conf.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +cat < /redis.conf +requirepass $REDISPASS +EOF + +exec redis-server /redis.conf diff --git a/data/conf/rspamd/dynmaps/aliasexp.php b/data/conf/rspamd/dynmaps/aliasexp.php index 947a02444..824037cf1 100644 --- a/data/conf/rspamd/dynmaps/aliasexp.php +++ b/data/conf/rspamd/dynmaps/aliasexp.php @@ -25,6 +25,7 @@ catch (PDOException $e) { // Init Redis $redis = new Redis(); $redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); function parse_email($email) { if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; diff --git a/data/conf/rspamd/dynmaps/forwardinghosts.php b/data/conf/rspamd/dynmaps/forwardinghosts.php index 10285b715..2186d7f26 100644 --- a/data/conf/rspamd/dynmaps/forwardinghosts.php +++ b/data/conf/rspamd/dynmaps/forwardinghosts.php @@ -4,6 +4,7 @@ ini_set('error_reporting', 0); $redis = new Redis(); $redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); function in_net($addr, $net) { $net = explode('/', $net); diff --git a/data/conf/rspamd/meta_exporter/pipe.php b/data/conf/rspamd/meta_exporter/pipe.php index 1858ee668..4d8e2a132 100644 --- a/data/conf/rspamd/meta_exporter/pipe.php +++ b/data/conf/rspamd/meta_exporter/pipe.php @@ -24,6 +24,7 @@ catch (PDOException $e) { // Init Redis $redis = new Redis(); $redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); // Functions function parse_email($email) { @@ -96,10 +97,10 @@ $rcpt_final_mailboxes = array(); foreach (json_decode($rcpts, true) as $rcpt) { // Remove tag $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); - + // Break rcpt into local part and domain part $parsed_rcpt = parse_email($rcpt); - + // Skip if not a mailcow handled domain try { if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { @@ -243,7 +244,7 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { WHERE `rcpt` = :rcpt2 ORDER BY id DESC LIMIT :retention_size - ) x + ) x );'); $stmt->execute(array( ':rcpt' => $rcpt_final, diff --git a/data/conf/rspamd/meta_exporter/pipe_rl.php b/data/conf/rspamd/meta_exporter/pipe_rl.php index 5f7fd42c3..f9a21caf7 100644 --- a/data/conf/rspamd/meta_exporter/pipe_rl.php +++ b/data/conf/rspamd/meta_exporter/pipe_rl.php @@ -14,6 +14,7 @@ try { else { $redis->connect('redis-mailcow', 6379); } + $redis->auth(getenv("REDISPASS")); } catch (Exception $e) { exit; diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index f122b281a..af1b21ebc 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -24,6 +24,7 @@ catch (PDOException $e) { // Init Redis $redis = new Redis(); $redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); // Functions function parse_email($email) { diff --git a/data/web/_rspamderror.php b/data/web/_rspamderror.php index 6bdfb3495..4976e0b5e 100644 --- a/data/web/_rspamderror.php +++ b/data/web/_rspamderror.php @@ -7,6 +7,7 @@ try { else { $redis->connect('redis-mailcow', 6379); } + $redis->auth(getenv("REDISPASS")); } catch (Exception $e) { exit; diff --git a/data/web/admin.php b/data/web/admin.php index 2081f34a6..5a8895dee 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -106,7 +106,7 @@ $template_data = [ 'all_domains' => $all_domains, 'mailboxes' => $mailboxes, 'f2b_data' => $f2b_data, - 'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'], + 'f2b_banlist_url' => getBaseUrl() . "/f2b-banlist?id=" . $f2b_data['banlist_id'], 'q_data' => quarantine('settings'), 'qn_data' => quota_notification('get'), 'pw_reset_data' => reset_password('get_notification'), diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index 0b03eb3ce..9526313ae 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -19,6 +19,7 @@ try { else { $redis->connect('redis-mailcow', 6379); } + $redis->auth(getenv("REDISPASS")); } catch (Exception $e) { exit; diff --git a/data/web/f2b-banlist.php b/data/web/f2b-banlist.php new file mode 100644 index 000000000..05c769907 --- /dev/null +++ b/data/web/f2b-banlist.php @@ -0,0 +1,11 @@ + 'danger', 'log' => array(__FUNCTION__, $_action, $_data), @@ -188,7 +188,7 @@ function dkim($_action, $_data = null, $privkey = false) { return false; } } - if (!ctype_alnum($dkim_selector)) { + if (!ctype_alnum(str_replace(['-', '_', '.'], '', $dkim_selector))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_data), diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 711bd5787..d351eccf4 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -2917,6 +2917,8 @@ function reset_password($action, $data = null) { ':username' => $username )); + update_sogo_static_view($username); + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $action, $_data_log), diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index cd62d5d7c..c63a0d37b 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3440,7 +3440,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'old_maildir' => $domain . '/' . $old_local_part, 'new_maildir' => $domain . '/' . $new_local_part ); - docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + if (getenv("CLUSTERMODE") == "replication") { + // broadcast to each dovecot container + docker('broadcast', 'dovecot-mailcow', 'exec', $exec_fields); + } else { + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + } // rename username in sogo $exec_fields = array( diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index fd196f9e0..7fb619d44 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 = "15082024_1212"; + $db_version = "20112024_1105"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -111,6 +111,10 @@ function init_db_schema() { "c_name" => "VARCHAR(255) NOT NULL", "c_password" => "VARCHAR(255) NOT NULL DEFAULT ''", "c_cn" => "VARCHAR(255)", + "c_l" => "VARCHAR(255)", + "c_o" => "VARCHAR(255)", + "c_ou" => "VARCHAR(255)", + "c_telephonenumber" => "VARCHAR(255)", "mail" => "VARCHAR(255) NOT NULL", // TODO -> use TEXT and check if SOGo login breaks on empty aliases "aliases" => "TEXT NOT NULL", @@ -1019,7 +1023,7 @@ function init_db_schema() { ) ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), + ), "pushover" => array( "cols" => array( "username" => "VARCHAR(255) NOT NULL", @@ -1403,7 +1407,7 @@ function init_db_schema() { "key_size" => 2048, "max_quota_for_domain" => 10240 * 1048576, ) - ); + ); $default_mailbox_template = array( "template" => "Default", "type" => "mailbox", @@ -1438,7 +1442,7 @@ function init_db_schema() { "acl_quarantine_category" => 1, "acl_app_passwds" => 1, ) - ); + ); $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); $stmt->execute(array( ":type" => "domain", @@ -1452,8 +1456,8 @@ function init_db_schema() { ":type" => "domain", ":template" => $default_domain_template["template"], ":attributes" => json_encode($default_domain_template["attributes"]) - )); - } + )); + } $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); $stmt->execute(array( ":type" => "mailbox", @@ -1467,8 +1471,8 @@ function init_db_schema() { ":type" => "mailbox", ":template" => $default_mailbox_template["template"], ":attributes" => json_encode($default_mailbox_template["attributes"]) - )); - } + )); + } // remove old sogo views and triggers $pdo->query("DROP TRIGGER IF EXISTS sogo_update_password"); diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index e67271412..5738e7c0a 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -66,6 +66,7 @@ try { else { $redis->connect('redis-mailcow', 6379); } + $redis->auth(getenv("REDISPASS")); } catch (Exception $e) { // Stop when redis is not available diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 34ab963ff..0c9ffb3d4 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -692,8 +692,8 @@ jQuery(function($){ } else if (item.attributes.rl_frame === "d"){ item.attributes.rl_frame = lang_rl.day; } - item.attributes.rl_value = escapeHtml(item.attributes.rl_value); - + item.attributes.rl_value = (!item.attributes.rl_value) ? "∞" : escapeHtml(item.attributes.rl_value); + item.attributes.ratelimit = item.attributes.rl_value + " " + item.attributes.rl_frame; if (item.template.toLowerCase() == "default"){ item.action = '
' + @@ -817,14 +817,8 @@ jQuery(function($){ } }, { - title: 'rl_frame', - data: 'attributes.rl_frame', - defaultContent: '', - class: 'none', - }, - { - title: 'rl_value', - data: 'attributes.rl_value', + title: lang_edit.ratelimit, + data: 'attributes.ratelimit', defaultContent: '', class: 'none', }, @@ -893,7 +887,10 @@ jQuery(function($){ item.quota.value = humanFileSize(item.quota_used) + "/" + item.quota.value; item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); - item.last_mail_login = item.last_imap_login + '/' + item.last_pop3_login + '/' + item.last_smtp_login + '/' + item.last_sso_login; + item.last_mail_login = (item.attributes.imap_access == 1 ? '
IMAP @ ' + unix_time_format(Number(item.last_imap_login)) + '

' : '') + + (item.attributes.pop3_access == 1 ? '
POP3 @ ' + unix_time_format(Number(item.last_pop3_login)) + '

' : '') + + (item.attributes.smtp_access == 1 ? '
SMTP @ ' + unix_time_format(Number(item.last_smtp_login)) + '

' : '') + + '
SSO @ ' + unix_time_format(Number(item.last_sso_login)) + '
'; /* if (!item.rl) { item.rl = '∞'; @@ -1009,14 +1006,7 @@ jQuery(function($){ data: 'last_mail_login', searchable: false, defaultContent: '', - responsivePriority: 7, - render: function (data, type) { - res = data.split("/"); - return '
IMAP @ ' + unix_time_format(Number(res[0])) + '

' + - '
POP3 @ ' + unix_time_format(Number(res[1])) + '

' + - '
SMTP @ ' + unix_time_format(Number(res[2])) + '

' + - '
SSO @ ' + unix_time_format(Number(res[3])) + '
'; - } + responsivePriority: 7 }, { title: lang.last_pw_change, @@ -1191,7 +1181,8 @@ jQuery(function($){ } else if (item.attributes.rl_frame === "d"){ item.attributes.rl_frame = lang_rl.day; } - item.attributes.rl_value = escapeHtml(item.attributes.rl_value); + item.attributes.rl_value = (!item.attributes.rl_value) ? "∞" : escapeHtml(item.attributes.rl_value); + item.attributes.ratelimit = item.attributes.rl_value + " " + item.attributes.rl_frame; item.attributes.quota = humanFileSize(item.attributes.quota); @@ -1336,14 +1327,8 @@ jQuery(function($){ } }, { - title: "rl_frame", - data: 'attributes.rl_frame', - defaultContent: '', - class: 'none', - }, - { - title: 'rl_value', - data: 'attributes.rl_value', + title: lang_edit.ratelimit, + data: 'attributes.ratelimit', defaultContent: '', class: 'none', }, diff --git a/data/web/json_api.php b/data/web/json_api.php index 36b39b0e6..8c28a97e3 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -510,16 +510,6 @@ if (isset($_GET['query'])) { $_SESSION['challenge'] = $WebAuthn->getChallenge(); return; break; - case "fail2ban": - if (!isset($_SESSION['mailcow_cc_role'])){ - switch ($object) { - case 'banlist': - header('Content-Type: text/plain'); - echo fail2ban('banlist', 'get', $extra); - break; - } - } - break; } if (isset($_SESSION['mailcow_cc_role'])) { switch ($category) { @@ -1431,10 +1421,6 @@ if (isset($_GET['query'])) { break; case "fail2ban": switch ($object) { - case 'banlist': - header('Content-Type: text/plain'); - echo fail2ban('banlist', 'get', $extra); - break; default: $data = fail2ban('get'); process_get_return($data); diff --git a/data/web/lang/lang.fr-fr.json b/data/web/lang/lang.fr-fr.json index d87632c68..386a4dfc6 100644 --- a/data/web/lang/lang.fr-fr.json +++ b/data/web/lang/lang.fr-fr.json @@ -207,7 +207,7 @@ "include_exclude_info": "Par défaut - sans sélection - toutes les boîte de réception sont adressées", "includes": "Inclure ces destinataires", "last_applied": "Dernière application", - "license_info": "Une licence n’est pas requise, mais contribue au développement.
Enregistrer votre GUID ici or acheter le support pour votre intallation Mailcow.", + "license_info": "Une licence n’est pas requise, mais contribue au développement.
Enregistrer votre GUID ici ou acheter le support pour votre installation Mailcow.", "link": "Lien", "loading": "Veuillez patienter…", "logo_info": "Votre image sera redimensionnée à une hauteur de 40 pixels pour la barre de navigation du haut et à un maximum de 250 pixels en largeur pour la page d'accueil. Un graphique extensible est fortement recommandé.", diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json index fb35d213b..f64664aa7 100644 --- a/data/web/lang/lang.ru-ru.json +++ b/data/web/lang/lang.ru-ru.json @@ -14,6 +14,7 @@ "prohibited": "Запрещено правилами ACL", "protocol_access": "Настройка разрешенных протоколов", "pushover": "Pushover API", + "pw_reset": "Разрешить пользователям mailcow восстановление паролей", "quarantine": "Карантин - действия", "quarantine_attachments": "Карантин - вложения", "quarantine_category": "Категория уведомлений о спаме", @@ -28,8 +29,7 @@ "spam_score": "Политика фильтрации спама", "syncjobs": "Задания синхронизации", "tls_policy": "Политика шифрования", - "unlimited_quota": "Неограниченная квота для почтовых ящиков", - "pw_reset": "Разрешить сброс пароля пользователей mailcow" + "unlimited_quota": "Неограниченная квота для почтовых ящиков" }, "add": { "activate_filter_warn": "Активация этого фильтра отключит все остальные фильтры этого типа.", @@ -42,13 +42,14 @@ "alias_domain": "Псевдоним домена", "alias_domain_info": "Действительные имена доменов, раздёленные запятыми.", "app_name": "Название приложения", + "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", "app_password": "Добавить пароль приложения", "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Параметры резервного MX", "bcc_dest_format": "Место назначения BCC должно быть единственным действительным адресом электронной почты.
Если вам нужно отправить копию на несколько адресов - используйте псевдоним.", "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя.", "custom_params": "Пользовательские параметры", - "custom_params_hint": "Верно: --param=xy, не верно: --param xy", + "custom_params_hint": "Верно: --param=xy, неверно: --param xy", "delete1": "Удаление из источника после завершения", "delete2": "Удаление писем по месту назначения, которые не находятся на исходном", "delete2duplicates": "Удаление дубликатов по назначению", @@ -58,6 +59,7 @@ "domain": "Домен", "domain_matches_hostname": "Домен %s соответствует имени хоста", "domain_quota_m": "Квота домена (MiB)", + "dry": "Имитировать синхронизацию", "enc_method": "Метод шифрования", "exclude": "Исключить объекты (regex)", "full_name": "Полное имя", @@ -89,7 +91,7 @@ "relay_all_info": "↪Если вы решите не ретранслировать всех получателей, вам нужно будет добавить (\"слепой\") почтовый адрес для каждого получателя, которого следует ретранслировать.", "relay_domain": "Ретрансляция этого домена", "relay_transport_info": "
Инфо
Вы можете настроить собственный транспорт для домена. Если такой настройки нет, то доставка будет выполнена на основе MX записей.", - "relay_unknown_only": "Ретрансляция только не существующих почтовых ящиков. Почта к существующим почтовым ящикам будут доставляться локально.", + "relay_unknown_only": "Ретрансляция только несуществующих почтовых ящиков. Почта существующих почтовых ящиков будут доставляться локально.", "relayhost_wrapped_tls_info": "Пожалуйста не используйте TLS порты (в основном это 465 порт).
\r\nИспользуйте любой не TLS порт который поддерживает STARTTLS. А для защиты от downgrate атак - настройке принудительную политику TLS.", "select": "Пожалуйста, выберите...", "select_domain": "Пожалуйста, сначала выберите домен", @@ -99,6 +101,7 @@ "subscribeall": "Подписаться на все папки и подпапки", "syncjob": "Добавить задание синхронизации", "syncjob_hint": "Пароли к вашему аккаунту будут сохранены на сервере в виде простого текста!", + "tags": "Теги", "target_address": "Владельцы псевдонима", "target_address_info": "Адреса почтовых ящиков, разделенные запятыми.", "target_domain": "Целевой домен", @@ -106,10 +109,7 @@ "timeout2": "Тайм-аут для подключения к локальному хосту", "username": "Имя пользователя", "validate": "Проверить", - "validation_success": "Проверка прошла успешно", - "tags": "Теги", - "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", - "dry": "Имитировать синхронизацию" + "validation_success": "Проверка прошла успешно" }, "admin": { "access": "Настройки доступа", @@ -135,6 +135,8 @@ "admins": "Администраторы", "admins_ldap": "Администраторы LDAP", "advanced_settings": "Расширенные настройки", + "allowed_methods": "Access-Control-Allow-Methods", + "allowed_origins": "Access-Control-Allow-Origin", "api_allow_from": "Список IP-адресов для доступа к API (разделенных запятой или новой строкой)", "api_info": "API находится в стадии разработки. Документация находится по адресу /api", "api_key": "Ключ API", @@ -151,6 +153,8 @@ "change_logo": "Изменить логотип", "configuration": "Глобальные настройки", "convert_html_to_text": "Сконвертировать HTML в обычный текст", + "copy_to_clipboard": "Текст скопирован в буфер обмена!", + "cors_settings": "Настройки CORS", "credentials_transport_warning": "Предупреждение: добавление новой записи перезапишет учетные данные для всех записей с таким же следующим хостом.", "customer_id": "ID клиента", "customize": "Персонализация", @@ -179,10 +183,14 @@ "empty": "Пусто", "excludes": "Исключает этих получателей", "f2b_ban_time": "Время бана (в секундах)", + "f2b_ban_time_increment": "Время бана увеличивается с каждым баном", "f2b_blacklist": "Черный список подсетей/хостов", "f2b_filter": "Правила фильтрации с помощью регулярных выражений", "f2b_list_info": "Хосты или подсети, занесенные в черный список, всегда будут перевешивать объекты из белого списка. Обновление списка займет несколько секунд.", + "f2b_manage_external": "Внешнее управление Fail2Ban", + "f2b_manage_external_info": "Fail2ban по-прежнему будет вести банлист, но не будет активно устанавливать правила для блокировки трафика. Используйте сгенерированный ниже банлист для внешнего блокирования трафика.", "f2b_max_attempts": "Максимальное количество попыток", + "f2b_max_ban_time": "Максимальное время блокировки", "f2b_netban_ipv4": "Размер подсети IPv4 для применения бана (8-32)", "f2b_netban_ipv6": "Размер подсети IPv6 для применения бана (8-128)", "f2b_parameters": "Настройки Fail2ban", @@ -208,14 +216,19 @@ "include_exclude": "Включить/Исключить", "include_exclude_info": "По умолчанию - без выбора - все почтовые ящики адресованы", "includes": "Включить этих получателей", + "ip_check": "Проверить IP", + "ip_check_disabled": "Проверка IP-адресов отключена. Вы можете включить её в разделе
Система > Конфигурация > Параметры > Персонализация.", + "ip_check_opt_in": "Согласие на использование сторонних служб ipv4.mailcow.email и ipv6.mailcow.email для разрешения внешних IP-адресов.", "is_mx_based": "На основе MX", "last_applied": "Посл. применение", - "license_info": "Лицензия не обязательна, но её приобретение помогает дальнейшему развитию mailcow.
Зарегистрируйте свой GUID здесь или приобретите поддержку для вашей установки mailcow.", + "license_info": "Лицензия необязательна, но её приобретение помогает дальнейшему развитию mailcow.
Зарегистрируйте свой GUID здесь или приобретите поддержку для вашей установки mailcow.", "link": "Ссылка", "loading": "Пожалуйста, подождите...", "login_time": "Время входа", - "logo_info": "Ваше изображение будет масштабироваться до высоты 40px для верхней панели навигации и до 250px ширины для стартовой страницы.
Рекомендуется использовать векторную графику, на пример: .svg.", - "lookup_mx": "Назначение на основе резовинга MX записи по регулярному выражению (.*\\.example\\.com$ для маршрутизации всей почты через этот хост, если MX заканчивающийся на example.com)", + "logo_dark_label": "Инвертированный для темного режима", + "logo_info": "Ваше изображение будет масштабироваться до высоты 40px для верхней панели навигации и до 250px ширины для стартовой страницы.
Рекомендуется использовать векторную графику, например: .svg.", + "logo_normal_label": "Обычный", + "lookup_mx": "Назначение - регулярное выражение для сопоставления с именем MX (.*\\.google\\.com для направления всей почты, адресованной MX, заканчивающейся на google.com, через этот хоп)", "main_name": "Название для \"mailcow UI\"", "merged_vars_hint": "Серым цветом выделены строки полученные из vars.(local.)inc.php, они не могут быть изменены.", "message": "Сообщение", @@ -225,15 +238,16 @@ "no_active_bans": "В данный момент нет забаненных подсетей/хостов", "no_new_rows": "Нет доступных строк", "no_record": "Нет записей", - "oauth2_client_id": "ID клиента", - "oauth2_apps": "Приложения OAuth2", "oauth2_add_client": "Добавить клиента OAuth2", + "oauth2_apps": "Приложения OAuth2", + "oauth2_client_id": "ID клиента", "oauth2_client_secret": "Секретный ключ пользователя", "oauth2_info": "Реализация OAuth2 поддерживает предоставления кодов авторизации и выдает токены продления сессии.
\r\nСервер также автоматически выдает новый токен продления сессии, после того, как предыдущий был использован.

\r\n• Scope по умолчанию: profile. Только пользователи почтовых аккаунтов могут проходить аутентификацию через OAuth2. Если параметр области не указан, он возвращается к profile.
\r\n• Параметр state должен быть отправлен клиентом как часть запроса для авторизации.

\r\nПути для запросов OAuth2 API:
\r\n
    \r\n
  • Authorization endpoint: /oauth/authorize
  • \r\n
  • Token endpoint: /oauth/token
  • \r\n
  • Resource page: /oauth/profile
  • \r\n
\r\nГенерирование нового клиентского секрета не приводит к истечению существующих кодов авторизации, но они не смогут обновить свой токен.

\r\nОтзыв клиентских токенов приведет к немедленному прекращению всех активных сеансов. Все клиенты должны будут пройти повторную аутентификацию.", "oauth2_redirect_uri": "Переадресация URI", "oauth2_renew_secret": "Сгенерировать новый ключ клиента", "oauth2_revoke_tokens": "Отозвать все клиентские токены", "optional": "опционально", + "options": "Параметры", "password": "Пароль", "password_length": "Минимальная длина пароля", "password_policy": "Политика паролей", @@ -243,6 +257,11 @@ "password_policy_numbers": "Должен содержать цифру", "password_policy_special_chars": "Должны содержать специальный символ", "password_repeat": "Подтверждение пароля (повтор)", + "password_reset_info": "Если получатель не указан, использование данной функции недоступно.", + "password_reset_settings": "Параметры восстановления паролей", + "password_reset_tmpl_html": "Шаблон в виде HTML", + "password_reset_tmpl_text": "Шаблон в виде обычного текста", + "password_settings": "Параметры паролей", "priority": "Приоритет", "private_key": "Закрытый ключ", "quarantine": "Карантин", @@ -250,7 +269,7 @@ "quarantine_exclude_domains": "Исключить домены и псевдонимы доменов", "quarantine_max_age": "Максимальный период хранения в днях
Значение должно быть равно или больше 1 дня.", "quarantine_max_score": "Не уведомлять о спаме, если оценка письма выше, чем:
По умолчанию 9999.0", - "quarantine_max_size": "Максимальный размер в MiB (письма большего размера не будет сохранены):
0 означает, что карантин отключён.", + "quarantine_max_size": "Максимальный размер в MiB (письма большего размера не будут сохранены):
0 означает, что карантин отключён.", "quarantine_notification_html": "Шаблон уведомления:
Оставьте пустым, чтобы восстановить шаблон по умолчанию.", "quarantine_notification_sender": "Email-адрес для отправки уведомления", "quarantine_notification_subject": "Тема письма", @@ -259,6 +278,7 @@ "quarantine_release_format_att": "Как вложение", "quarantine_release_format_raw": "Оригинальное письмо", "quarantine_retention_size": "Количество писем, сохраняемых в карантине на аккаунт:
0 означает, что карантин отключён.", + "queue_unban": "разблокировать", "quota_notification_html": "Шаблон уведомления:
Оставьте пустым, чтобы восстановить шаблон по умолчанию.", "quota_notification_sender": "Email-адрес для отправки уведомления", "quota_notification_subject": "Тема письма", @@ -267,7 +287,7 @@ "quota_notifications_vars": "{{percent}} равно текущей квоте пользователя
{{username}} - имя почтового аккаунта", "r_active": "Включенные ограничения", "r_inactive": "Отключенные ограничения", - "r_info": "Не активные (серые) элементы списка ограничений - это не валидные ограничения, и они не могут быть перемещены.
Вы можете добавить новые элементы в inc/vars.local.inc.php чтобы иметь возможность настраивать их.", + "r_info": "Неактивные (серые) элементы списка ограничений - это некорректные ограничения, и они не могут быть перемещены.
Вы можете добавить новые элементы в inc/vars.local.inc.php чтобы иметь возможность настраивать их.", "rate_name": "Название очереди", "recipients": "Получатели", "refresh": "Обновить", @@ -282,6 +302,8 @@ "remove_row": "Удалить строку", "reset_default": "Восстановить по умолчанию", "reset_limit": "Удалить хэш", + "reset_password_vars": "{{link}} Сгенерированная ссылка для восстановление пароля
{{username}} Имя почтового ящика пользователя, запросившего восстановление пароля
{{username2}} Имя почтового ящика для восстановления
{{date}} Дата запроса на восстановление пароля
{{token_lifetime}} Срок действия токена в минутах
{{hostname}} Имя хоста mailcow", + "restore_template": "Оставьте пустым, чтобы восстановить шаблон по умолчанию.", "routing": "Маршрутизация", "rsetting_add_rule": "Добавить правило", "rsetting_content": "Содержание правила", @@ -290,14 +312,14 @@ "rsetting_none": "Нет доступных правил", "rsettings_insert_preset": "Вставить пример \"%s\"", "rsettings_preset_1": "Отключить все, кроме DKIM и ограничения скорости для аутентифицированных пользователей", - "rsettings_preset_2": "Не проверять письма на спам Postmaster", + "rsettings_preset_2": "Не проверять письма Postmaster на спам", "rsettings_preset_3": "Разрешить только определённых отправителей для почтового ящика (использование только в качестве внутреннего почтового ящика)", "rsettings_preset_4": "Отключить Rspamd для домена", "rspamd_com_settings": "Имена правил будут сгенерированы на основе их ID.
Инструкция доступна на сайте документация Rspamd user settings, заготовленные шаблоны:", "rspamd_global_filters": "Глобальные правила фильтрации", "rspamd_global_filters_agree": "Я понимаю, что я делаю, и буду осторожен!", "rspamd_global_filters_info": "Глобальные правила фильтрации содержат различные виды глобальных черных и белых списков.", - "rspamd_global_filters_regex": "Названия фильтров отражают их предназначение. Все правила должены состоять из регулярных выражений в формате \"/pattern/options\" (на пример: /.+@domain\\.tld/i).
\r\nНесмотря на то, что перед сохранением правил выполняется проверка регулярных выражений, функциональность Rspamds может быть нарушена, если будет использован
\r\n некорректный синтаксис. Будьте внимательны при написании правил.
Электронные письма от адресов электронной почты, проходящие по регулярным выражениям черных списков, будут отклонены без сохранения в карантин.
\r\n Rspamd попытается прочитать содержимое правил при их изменении. Но, если что, вы можете перезапустить Rspamd, чтобы принять последние изменения принудительно.", + "rspamd_global_filters_regex": "Названия фильтров отражают их предназначение. Все правила должены состоять из регулярных выражений в формате \"/pattern/options\" (например: /.+@domain\\.tld/i).
\r\nНесмотря на то, что перед сохранением правил выполняется проверка регулярных выражений, функциональность Rspamds может быть нарушена, если будет использован
\r\n некорректный синтаксис. Будьте внимательны при написании правил.
Электронные письма от адресов электронной почты, проходящие по регулярным выражениям черных списков, будут отклонены без сохранения в карантин.
\r\n Rspamd попытается прочитать содержимое правил при их изменении. Но, если что, вы можете перезапустить Rspamd, чтобы принять последние изменения принудительно.", "rspamd_settings_map": "Правила Rspamd", "sal_level": "Уровень Муу", "save": "Сохранить изменения", @@ -337,19 +359,7 @@ "username": "Имя пользователя", "validate_license_now": "Получить лицензию на основе GUID с сервера лицензий", "verify": "Проверить", - "yes": "✓", - "queue_unban": "разблокировать", - "f2b_ban_time_increment": "Время бана увеличивается с каждым баном", - "f2b_max_ban_time": "Максимальное время блокировки", - "allowed_origins": "Access-Control-Allow-Origin", - "cors_settings": "Настройки CORS", - "allowed_methods": "Access-Control-Allow-Methods", - "ip_check": "Проверить IP", - "ip_check_disabled": "Проверка IP отключена. Вы можете включить его в разделе
Система > Конфигурация > Параметры > Настроить.", - "ip_check_opt_in": "Согласие на использование сторонних служб ipv4.mailcow.email и ipv6.mailcow.email для разрешения внешних IP-адресов.", - "f2b_manage_external": "Внешнее управление Fail2Ban", - "f2b_manage_external_info": "Fail2ban по-прежнему будет вести банлист, но не будет активно устанавливать правила для блокировки трафика. Используйте сгенерированный ниже банлист для внешнего блокирования трафика.", - "copy_to_clipboard": "Текст скопирован в буфер обмена!" + "yes": "✓" }, "danger": { "access_denied": "Доступ запрещён, или указаны неверные данные", @@ -365,7 +375,10 @@ "bcc_exists": "Для типов %s уже существует карта BCC %s", "bcc_must_be_email": "Назначение BCC %s не является правильным адресом электронной почты", "comment_too_long": "Комментарий слишком длинный, придел 160 символов", + "cors_invalid_method": "Указан недопустимый метод разрешения", + "cors_invalid_origin": "Указан неверный Allow-Origin", "defquota_empty": "Квота по умолчанию не может быть 0.", + "demo_mode_enabled": "Демонстрационный режим включен", "description_invalid": "Недопустимое описание ресурса %s", "dkim_domain_or_sel_exists": "Ключ DKIM для \"%s\" уже существует", "dkim_domain_or_sel_invalid": "DKIM домен или селектор недопустимы для %s", @@ -375,20 +388,23 @@ "domain_not_empty": "Нельзя удалить непустой домен %s", "domain_not_found": "Домен %s не найден", "domain_quota_m_in_use": "Квота домена должна быть больше или равна %s MiB", - "extra_acl_invalid": "Адрес внешнего отправителя \"%s\" не валидный.", - "extra_acl_invalid_domain": "Адрес внешнего отправителя \"%s\" не валидный домен", + "extended_sender_acl_denied": "отсутствует ACL для установки внешних адресов отправителей", + "extra_acl_invalid": "Адрес внешнего отправителя \"%s\" некорректен", + "extra_acl_invalid_domain": "Адрес внешнего отправителя \"%s\" содержит некорректный домен", "fido2_verification_failed": "Ошибка валидации FIDO2: %s", "file_open_error": "Файл не может быть открыт на запись", "filter_type": "Неверный тип фильтра", "from_invalid": "Отправитель не может быть пустым", "global_filter_write_error": "Ошибка записи фильтра в файл: %s", - "global_map_invalid": "Идентификатор глобального правила %s не валидный", + "global_map_invalid": "Недопустимый идентификатор глобального правила %s", "global_map_write_error": "Не удалось создать глобальное правило ID %s: %s", - "goto_empty": "Псевдоним должен содержать по крайней мере один валидный адрес владельца", + "goto_empty": "Псевдоним должен содержать по крайней мере один действующий адрес владельца", "goto_invalid": "Недопустимый основной адрес %s", "ham_learn_error": "Ошибка при обучении полезной почты: %s", "imagick_exception": "Ошибка в Imagick при чтении изображения", + "img_dimensions_exceeded": "Разрешение изображения превышает допустимое значение", "img_invalid": "Невозможно проверить файл изображения", + "img_size_exceeded": "Изображение превышает допустимый размер файла", "img_tmp_missing": "Невозможно проверить файл изображения: временный файл не найден", "invalid_bcc_map_type": "Неверный тип правила BCC", "invalid_destination": "Назначение \"%s\" указано неверно", @@ -397,8 +413,9 @@ "invalid_mime_type": "Неверный mime type", "invalid_nexthop": "Формат следующего хоста неверен", "invalid_nexthop_authenticated": "Следующий хост существует с разными данными авторизации, пожалуйста, обновите существующие данные авторизации сначала для этого хоста.", - "invalid_recipient_map_new": "Новый получатель: %s не валидный", - "invalid_recipient_map_old": "Первоначальный получатель: %s не валидный", + "invalid_recipient_map_new": "Недопустимый новый получатель: %s", + "invalid_recipient_map_old": "Недопустимый исходный получатель: %s", + "invalid_reset_token": "Неверный токен восстановления", "ip_list_empty": "Список разрешенных IP адресов не может быть пустым", "is_alias": "%s уже известен как псевдоним адреса", "is_alias_or_mailbox": "%s уже известен как псевдоним или почтовый аккаунт", @@ -418,7 +435,7 @@ "max_quota_in_use": "Квота почтового аккаунта должна быть больше или равна %d MiB", "maxquota_empty": "Максимальная квота почтового аккаунта не должна быть 0.", "mysql_error": "Ошибка в MySQL: %s", - "network_host_invalid": "Сеть или хост: %s не валидный", + "network_host_invalid": "Недопустимые сеть или хост: %s", "next_hop_interferes": "%s пересекается с %s", "next_hop_interferes_any": "Существующий хост пересекается с %s", "nginx_reload_failed": "Обновление конфигурации Nginx не удалось: %s", @@ -428,6 +445,8 @@ "password_complexity": "Пароль не соответствует требованиям", "password_empty": "Пароль не может быть пустым", "password_mismatch": "Введенные пароли не совпадают", + "password_reset_invalid_user": "Почтовый ящик не найден или не задан адрес электронной почты для восстановления", + "password_reset_na": "Восстановление пароля в настоящее время недоступно. Пожалуйста, свяжитесь с вашим администратором.", "policy_list_from_exists": "Запись с указанным именем уже существует", "policy_list_from_invalid": "Запись имеет недопустимый формат", "private_key_error": "Ошибка приватного ключа: %s", @@ -436,17 +455,19 @@ "pushover_token": "Токен Pushover указан в неверном формате", "quota_not_0_not_numeric": "Размер квоты должен быть больше или равен нулю", "recipient_map_entry_exists": "Правило перезаписи \"%s\" уже существует", + "recovery_email_failed": "Не удалось отправить письмо для восстановления. Пожалуйста, свяжитесь с вашим администратором.", "redis_error": "Ошибка в Redis: %s", - "relayhost_invalid": "Правило %s не валидное", + "relayhost_invalid": "Недопустимое правило %s", "release_send_failed": "Сообщение не может быть восстановлено: %s", "reset_f2b_regex": "Сброс фильтров не был выполнен за отведённый промежуток времени, пожалуйста, повторите попытку или подождите еще несколько секунд и перезагрузите веб страницу.", + "reset_token_limit_exceeded": "Превышен лимит запросов на восстановление. Пожалуйста, попробуйте ещё раз позже.", "resource_invalid": "Недопустимое имя ресурса", "rl_timeframe": "Не верный временной интервал для лимита отправки", "rspamd_ui_pw_length": "Длина пароля должна составлять не менее 6 символов для Rspamd UI", "script_empty": "Скрипт не может быть пустым", "sender_acl_invalid": "Недопустимое значение ACL для: %s", "set_acl_failed": "Не удалось установить ACL", - "settings_map_invalid": "Правило ID: %s не валидное", + "settings_map_invalid": "Недопустимое правило ID %s", "sieve_error": "Ошибка в синтаксисе Sieve: %s", "spam_learn_error": "Ошибка при обучении спам фильтра: %s", "subject_empty": "Тема письма не может быть пустой", @@ -454,34 +475,67 @@ "targetd_not_found": "Основной домен %s не найден", "targetd_relay_domain": "Целевой домен %s уже является домен ретрансляции", "temp_error": "Временная ошибка", - "text_empty": "Текст не должен быть пустым", + "template_exists": "Шаблон %s уже существует", + "template_id_invalid": "Недопустимое значение ID шаблона: %s", + "template_name_invalid": "Недопустимое название шаблона", + "text_empty": "Текст не может быть пустым", "tfa_token_invalid": "Неправильный TFA токен", "tls_policy_map_dest_invalid": "Недопустимое значение назначения политики", "tls_policy_map_entry_exists": "Правило политики шифрования \"%s\" уже существует", "tls_policy_map_parameter_invalid": "Недопустимое значение параметра политики", + "to_invalid": "Получатель не может быть пустым", "totp_verification_failed": "Ошибка валидации TOTP", "transport_dest_exists": "Назначение для отправки \"%s\" уже существует", - "webauthn_verification_failed": "Ошибка валидации WebAuthn: %s", "unknown": "Произошла неизвестная ошибка", "unknown_tfa_method": "Неизвестный метод TFA", "unlimited_quota_acl": "Неограниченная квота запрещена политикой доступа", "username_invalid": "Имя пользователя %s нельзя использовать", "validity_missing": "Пожалуйста, назначьте срок действия", "value_missing": "Пожалуйста заполните все поля", - "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s", - "cors_invalid_method": "Указан недопустимый метод разрешения", - "demo_mode_enabled": "Демонстрационный режим включен", - "cors_invalid_origin": "Указан неверный Allow-Origin" + "webauthn_authenticator_failed": "Выбранный аутентификатор не был найден", + "webauthn_publickey_failed": "Для выбранного аутентификатора не был сохранен открытый ключ", + "webauthn_username_failed": "Выбранный аутентификатор принадлежит другой учетной записи", + "webauthn_verification_failed": "Ошибка валидации WebAuthn: %s", + "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s" + }, + "datatables": { + "collapse_all": "Свернуть все", + "decimal": ",", + "emptyTable": "В таблице отсутствуют данные", + "expand_all": "Развернуть все", + "info": "Показаны записи с _START_ по _END_ из _TOTAL_", + "infoEmpty": "Показано 0 записей", + "infoFiltered": "(отфильтровано из _MAX_ всех записей)", + "infoPostFix": "", + "lengthMenu": "Показать _MENU_ записей", + "loadingRecords": "Загрузка...", + "processing": "Пожалуйста, подождите...", + "search": "Поиск:", + "thousands": " ", + "zeroRecords": "Не найдено соответствующих записей", + "paginate": { + "first": "Первая", + "last": "Последняя", + "next": "Следующая", + "previous": "Предыдущая" + }, + "aria": { + "sortAscending": ": активируйте для сортировки столбца по возрастанию", + "sortDescending": ": активируйте для сортировки столбца по убыванию" + } }, "debug": { + "architecture": "Архитектура", "chart_this_server": "Диаграмма (текущий сервер)", - "containers_info": "Статус контейнеров Docker", - "container_running": "Работающий", "container_disabled": "Контейнер остановлен или отключен", + "container_running": "Работающий", "container_stopped": "Остановлен", + "containers_info": "Статус контейнеров Docker", + "cores": "яд.", "current_time": "Системное время", "disk_usage": "Использование дискового пространства", "docs": "Проиндексировано объектов", + "error_show_ip": "Не удалось определить публичные IP-адреса", "external_logs": "Внешние журналы", "history_all_servers": "История (все серверы)", "in_memory_logs": "Журналы контейнеров", @@ -490,9 +544,12 @@ "log_info": "

Журналы контейнеров mailcow сохраняются в Redis, и раз в минуту строки журнала за пределами LOG_LINES (%d) удаляются, чтобы уменьшить нагрузку на сервер.\r\n
Сами журналы контейнеров не сохраняются после перезагрузки контейнера. Все контейнеры дополнительно пишут логи в службу Docker, и, следовательно, используют драйвер логирования по умолчанию. Журналы контейнеров предусмотрены только для отладки мелких проблем. Для других задач, пожалуйста, настройте драйвер логирования Docker самостоятельно.

\r\n

Внешние журналы собираются через API приложений.

\r\n

Статические журналы – это, в основном, журналы активности, которые не записываются в Dockerd, но все равно должны быть постоянными (за исключением журналов API).

", "login_time": "Время входа", "logs": "Журналы", + "memory": "Память", + "no_update_available": "Система обновлена до последней версии", "online_users": "Подключено пользователей", "restart_container": "Перезапустить", "service": "Сервис", + "show_ip": "Показать публичные IP-адреса", "size": "Индексы занимают", "solr_dead": "Solr не запущен. Если вы включили Solf в файле настроек mailcow.conf и это сообщение отображается более получаса, скорее всего Solr сломан.", "solr_status": "Состояние Solr", @@ -500,10 +557,13 @@ "started_on": "Запущен в", "static_logs": "Статические журналы", "success": "Успех", - "no_update_available": "Система обновлена до последней версии", "system_containers": "Система и контейнеры", + "timezone": "Часовой пояс", + "update_available": "Доступно обновление", + "update_failed": "Не удалось проверить наличие обновлений", "uptime": "Время работы", - "username": "Имя пользователя" + "username": "Имя пользователя", + "wip": "В настоящее время идёт разработка" }, "diagnostics": { "cname_from_a": "Значение, полученное из записи A/AAAA. Это поддерживается до тех пор, пока запись указывает на правильный ресурс.", @@ -514,7 +574,7 @@ "dns_records_name": "Название", "dns_records_status": "Статус", "dns_records_type": "Тип", - "optional": "Эта запись не обязательна." + "optional": "Эта запись необязательна." }, "edit": { "acl": "ACL (Список прав)", @@ -527,6 +587,7 @@ "allowed_protocols": "Разрешённые протоколы", "app_name": "Название приложения", "app_passwd": "Пароль приложения", + "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Параметры резервного копирования MX", "bcc_dest_format": "Назначением для правила BCC должен быть единственный действительный адрес электронной почты.", @@ -534,6 +595,7 @@ "client_secret": "Секретный ключ пользователя", "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя", "created_on": "Дата создания", + "custom_attributes": "Пользовательские атрибуты", "delete1": "Удаление из источника после завершения", "delete2": "Удаление писем по месту назначения, которые не находятся на исходном", "delete2duplicates": "Удаление дубликатов по назначению", @@ -542,6 +604,19 @@ "disable_login": "Вход в систему запрещен", "domain": "Изменение домена", "domain_admin": "Изменение администратора домена", + "domain_footer": "Нижний колонтитул домена", + "domain_footer_html": "HTML нижний колонтитул", + "domain_footer_info": "Нижние колонтитулы на уровне домена добавляются ко всем исходящим электронным письмам, связанным с адресом в этом домене.
Для нижнего колонтитула можно использовать следующие переменные:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Аутентифицированное имя пользователя, указанное MTA", + "custom": "{= foo =} - Если почтовый ящик имеет пользовательский атрибут \"foo\" со значением \"bar\", он возвращает \"bar\".", + "from_addr": "{= from_addr =} - Из адресной части envelope", + "from_domain": "{= from_domain =} - из доменной части envelope", + "from_name": "{= from_name =} - Из названия envelope, например, для \"Mailcow <moo@mailcow.tld>\" возвращается \"Mailcow\"", + "from_user": "{= from_user =} - Из пользовательской части envelope, например, для \"moo@mailcow.tld\" возвращается \"moo\"" + }, + "domain_footer_plain": "ПРОСТОЙ нижний колонтитул", + "domain_footer_skip_replies": "Ignore footer on reply e-mails", "domain_quota": "Квота домена", "domains": "Домены", "dont_check_sender_acl": "Отключить проверку отправителя для домена %s и псевдонимов домена", @@ -550,9 +625,9 @@ "exclude": "Исключить объекты (regex)", "extended_sender_acl": "Внешние адреса почты", "extended_sender_acl_info": "Для внешних доменов должен быть импортирован или сгенерирован доменный ключ DKIM с соответствующей записью TXT в домене, если внешний домен использует DMARC.
\r\n Не забудьте добавить этот сервер к соответствующей записи SPF TXT внешнего домена.
\r\n Добавление домена из списка внешних адресов в mailcow автоматически удалит соответствующие записи из внешних адресов пользователей.
\r\n Чтобы разрешить пользователю отправку от имени *@domain.tld, укажите @domain.tld.", + "footer_exclude": "Исключить из нижнего колонтитула", "force_pw_update": "Требовать смены пароля при следующем входе в систему", "force_pw_update_info": "Пользователь должен будет войти в %s и сменить свой пароль. mailcow OAuth2, SOGo, EAS, IMAP/POP3 и SMTP будут не доступны до смены пароля.", - "footer_exclude": "Исключить из нижнего колонтитула", "full_name": "Полное имя", "gal": "GAL - Глобальная адресная книга", "gal_info": "GAL содержит все объекты домена и не подлежит редактированию. Информация о занятости в SOGo будет отсутствовать для домена, если данная функция будет отключена! Требуется перезапустить SOGo, чтобы применить изменения.", @@ -566,6 +641,11 @@ "mailbox": "Изменение почтового аккаунта", "mailbox_quota_def": "Квота по умолчанию", "mailbox_relayhost_info": "Применяется только к почтовому ящику и личным псевдонимам, вне зависимости от настроек маршрутизации на уровне домена.", + "mailbox_rename": "Переименовать почтовый ящик", + "mailbox_rename_agree": "Я сделал резервную копию.", + "mailbox_rename_alias": "Автоматически создать псевдоним", + "mailbox_rename_title": "Новое имя локального почтового ящика", + "mailbox_rename_warning": "ВАЖНО! Перед переименованием почтового ящика создайте резервную копию.", "max_aliases": "Максимум псевдонимов", "max_mailboxes": "Максимум почтовых ящиков", "max_quota": "Максимальная квота почтового аккаунта (MiB)", @@ -574,9 +654,10 @@ "mbox_rl_info": "Этот лимит применяется к SASL логину пользователя и соответствует любому адресу отправителя, используемому зарегистрированным пользователем. Лимит скорости почтового аккаунта перекрывает лимит скорости для всего домена.", "mins_interval": "Интервал (в минутах)", "multiple_bookings": "Несколько бронирований", - "none_inherit": "Отсутствует / Наследуется", "nexthop": "Следующий хост", + "none_inherit": "Отсутствует / Наследуется", "password": "Пароль", + "password_recovery_email": "Адрес для восстановления пароля", "password_repeat": "Подтверждение пароля (повтор)", "previous": "Предыдущая страница", "private_comment": "Приватный комментарий", @@ -587,6 +668,7 @@ "pushover_only_x_prio": "Получать уведомления только об письмах с высоким приоритетом [X-Priority: 1]", "pushover_sender_array": "Получать уведомления от списка адресов электронной почты (envelop-from разделенные запятыми):", "pushover_sender_regex": "Получать уведомления от отправителей, удовлетворяющих regex-выражению:", + "pushover_sound": "Звук уведомления", "pushover_text": "Текст уведомления", "pushover_title": "Заголовок уведомления", "pushover_vars": "Когда фильтрация по отправителю не определена, уведомления будут доставлятся от всех отправителей.
Можно использовать обычный фильтр по отправителю и расширенный regex-фильтр, а также оба сразу.
Пожалуйста, ознакомьтесь с Pushover Privacy Policy перед использованием шаблонов для текста и заголовка", @@ -600,7 +682,7 @@ "relay_all_info": "↪Если вы решите не ретранслировать всех получателей, вам нужно будет добавить (\"слепой\") почтовый аккаунт для каждого получателя, которого следует ретранслировать.", "relay_domain": "Ретрансляция этого домена", "relay_transport_info": "
Инфо
Вы можете настроить собственный транспорт для домена. Если такой настройки нет, то доставка будет выполнена на основе MX записей.", - "relay_unknown_only": "Ретрансляция только не существующих почтовых ящиков. Почта к существующим почтовым ящикам будут доставляться локально.", + "relay_unknown_only": "Ретрансляция только несуществующих почтовых ящиков. Почта к существующим почтовым ящикам будут доставляться локально.", "relayhost": "Маршрутизация на основе отправителя", "remove": "Удалить", "resource": "Ресурс", @@ -612,6 +694,8 @@ "sieve_desc": "Краткое описание", "sieve_type": "Тип фильтра", "skipcrossduplicates": "Пропускать повторяющиеся сообщения в папках", + "sogo_access": "Прямая переадресация в SOGo", + "sogo_access_info": "После входа в систему пользователь автоматически перенаправляется в SOGo.", "sogo_visible": "Отображать псевдоним в SOGo", "sogo_visible_info": "Влияет только на объекты, которые могут отображаться в SOGo (персональные или общие псевдонимы, указывающие как минимум на один локальный почтовый аккаунт). Учтите, что если функция отключена, у пользователей не будет возможности выбрать адрес псевдонима в качестве отправителя в SOGo.", "spam_alias": "Создать или изменить временные (спам) псевдонимы", @@ -627,23 +711,7 @@ "title": "Изменение объекта", "unchanged_if_empty": "Если без изменений - оставьте пустым", "username": "Имя пользователя", - "validate_save": "Подтвердить и сохранить", - "sogo_access": "Прямая переадресация в SOGo", - "sogo_access_info": "После входа в систему пользователь автоматически перенаправляется в SOGo.", - "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", - "domain_footer_info": "Нижние колонтитулы на уровне домена добавляются ко всем исходящим электронным письмам, связанным с адресом в этом домене.
Для нижнего колонтитула можно использовать следующие переменные:", - "domain_footer_info_vars": { - "from_name": "{= from_name =} - Из названия envelope, например, для \"Mailcow <moo@mailcow.tld>\" возвращается \"Mailcow\"", - "auth_user": "{= auth_user =} - Аутентифицированное имя пользователя, указанное MTA", - "from_user": "{= from_user =} - Из пользовательской части envelope, например, для \"moo@mailcow.tld\" возвращается \"moo\"", - "from_addr": "{= from_addr =} - Из адресной части envelope", - "from_domain": "{= from_domain =} - из доменной части envelope", - "custom": "{= foo =} - Если почтовый ящик имеет пользовательский атрибут \"foo\" со значением \"bar\", он возвращает \"bar\"." - }, - "domain_footer": "Нижний колонтитул домена", - "domain_footer_html": "HTML нижний колонтитул", - "domain_footer_plain": "ПРОСТОЙ нижний колонтитул", - "custom_attributes": "Пользовательские атрибуты" + "validate_save": "Подтвердить и сохранить" }, "fido2": { "confirm": "Подтвердить", @@ -678,10 +746,10 @@ "header": { "administration": "Настройка сервера", "apps": "Приложения", - "debug": "Состояние сервера", + "debug": "Информация", "email": "E-Mail", - "mailcow_system": "Система", "mailcow_config": "Конфигурация", + "mailcow_system": "Система", "quarantine": "Карантин", "restart_netfilter": "Перезапустить netfilter", "restart_sogo": "Перезапустить SOGo", @@ -693,12 +761,19 @@ "session_expires": "Ваш сеанс закончится примерно через 15 секунд" }, "login": { + "back_to_mailcow": "Вернуться к mailcow", "delayed": "Вход был отложен на %s секунд.", "fido2_webauthn": "FIDO2/WebAuthn Login", + "forgot_password": "> Забыли пароль?", + "invalid_pass_reset_token": "Токен восстановления пароля недействителен или срок его действия истек.
Пожалуйста, запросите новую ссылку для восстановления пароля.", "login": "Войти", "mobileconfig_info": "Пожалуйста, войдите в систему как пользователь почтового аккаунта для загрузки профиля подключения Apple.", + "new_password": "Новый пароль", + "new_password_confirm": "Повторите новый пароль", "other_logins": "Вход с помощью ключа", "password": "Пароль", + "request_reset_password": "Запросить восстановление пароля", + "reset_password": "Восстановление пароля", "username": "Имя пользователя" }, "mailbox": { @@ -716,6 +791,7 @@ "add_mailbox": "Добавить почтовый аккаунт", "add_recipient_map_entry": "Добавить перезапись получателя", "add_resource": "Добавить ресурс", + "add_template": "Добавить шаблон", "add_tls_policy_map": "Добавить политику TLS", "address_rewriting": "Перезапись адресов", "alias": "Псевдоним", @@ -740,12 +816,12 @@ "bcc_to_rcpt": "Переключиться на тип \"получатель\"", "bcc_to_sender": "Переключиться на тип \"отправитель\"", "bcc_type": "Тип BCC", - "booking_null": "Всегда показывать как свободный", "booking_0_short": "Всегда свободнен", "booking_custom": "Лимит на количество бронирований", "booking_custom_short": "Жесткий лимит", - "booking_ltnull": "Неограниченный, занят при бронировании", "booking_lt0_short": "Неограниченный лимит", + "booking_ltnull": "Неограниченный, занят при бронировании", + "booking_null": "Всегда показывать как свободный", "catch_all": "Catch-all", "created_on": "Дата создания", "daily": "Раз в день", @@ -760,6 +836,7 @@ "domain_aliases": "Псевдонимы доменов", "domain_quota": "Квота", "domain_quota_total": "Квота домена", + "domain_templates": "Шаблоны доменов", "domains": "Домены", "edit": "Изменить", "empty": "Пусто", @@ -787,6 +864,7 @@ "mailbox_defaults_info": "Установите настройки по умолчанию для новых почтовых аккаунтов.", "mailbox_defquota": "Квота по умолчанию", "mailbox_quota": "Макс. квота почт. ящика", + "mailbox_templates": "Шаблоны почтовых ящиков", "mailboxes": "Почтовые ящики", "max_aliases": "Максимум псевдонимов", "max_mailboxes": "Максимум почтовых ящиков", @@ -814,16 +892,17 @@ "recipient_map_new": "Перезапись на", "recipient_map_new_info": "Должен быть действующим почтовым ящиком.", "recipient_map_old": "Получатель", - "recipient_map_old_info": "Должен быть валидный почтовым ящиком или доменом.", + "recipient_map_old_info": "Должен быть действующим почтовым ящиком или доменом.", "recipient_maps": "Перезапись получателя", "relay_all": "Ретрансляция всех получателей", + "relay_unknown": "Ретрансляция неизвестных получателей", "remove": "Удалить", "resources": "Ресурсы", "running": "В процессе", "sender": "Отправитель", "set_postfilter": "Использовать как постфильтр", "set_prefilter": "Использовать как предварительный фильтр", - "sieve_info": "Вы можете сохранить несколько фильтров для каждого пользователя, но только один предварительный фильтр и один постфильтр могут быть активными одновременно.
\r\n Каждый фильтр будет обработан в описанном порядке. Не сломанный скрипт, не keep; не остановит обработку дальнейших скриптов.

Global sieve prefilter • Prefilter • User scripts • Postfilter • Global sieve postfilter", + "sieve_info": "Вы можете сохранить несколько фильтров для каждого пользователя, но только один предварительный фильтр и один постфильтр могут быть активными одновременно.
\r\n Каждый фильтр будет обработан в описанном порядке. Ни сломанный скрипт, ни keep; не остановит обработку дальнейших скриптов.

Global sieve prefilter • Prefilter • User scripts • Postfilter • Global sieve postfilter", "sieve_preset_1": "Discard mail with probable dangerous file types", "sieve_preset_2": "Always mark the e-mail of a specific sender as seen", "sieve_preset_3": "Discard silently, stop all further sieve processing", @@ -831,7 +910,7 @@ "sieve_preset_5": "Auto responder (vacation)", "sieve_preset_6": "Reject mail with response", "sieve_preset_7": "Redirect and keep/drop", - "sieve_preset_8": "Discard message sent to an alias address the sender is part of", + "sieve_preset_8": "Переслать письмо от определенного отправителя, пометить его как прочитанное и поместить в подпапку", "sieve_preset_header": "Пожалуйста, ознакомьтесь с примерами ниже. Для более подробной информации прочитайте Sieve Wikipedia.", "sogo_visible": "Отображать псевдоним в SOGo", "sogo_visible_n": "Не отображать псевдоним в SOGo", @@ -840,25 +919,27 @@ "stats": "Статистика", "status": "Статус", "sync_jobs": "Задания синхронизации", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", + "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", + "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", + "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", + "syncjob_EX_OK": "Успешно", "syncjob_check_log": "Проверить журнал", "syncjob_last_run_result": "Результат последнего запуска", - "syncjob_EX_OK": "Успешно", - "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", - "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", - "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", "table_size": "Размер таблицы", "table_size_show_n": "Отображать %s полей", "target_address": "Владельцы псевдонима", "target_domain": "Целевой домен", + "template": "Шаблон", + "templates": "Шаблоны", "tls_enforce_in": "Принудительный TLS (входящие)", "tls_enforce_out": "Принудительный TLS (исходящие)", "tls_map_dest": "Назначение", "tls_map_dest_info": "пример: example.org, .example.org, [mail.example.org]:25", "tls_map_parameters": "Параметры", - "tls_map_parameters_info": "Оставьте поле пустым или укажите параметры, на пример: protocols=!SSLv2 ciphers=medium exclude=3DES", + "tls_map_parameters_info": "Оставьте поле пустым или укажите параметры, например: protocols=!SSLv2 ciphers=medium exclude=3DES", "tls_map_policy": "Политика", "tls_policy_maps": "Правила TLS", "tls_policy_maps_enforced_tls": "Для исходящих сообщений от пользователей с включенной принудительной политикой шифрования исходящих соединений не описанные глобальной политикой,
\r\n будут применены значения по умолчанию, указанные в smtp_tls_mandatory_protocols и smtp_tls_mandatory_ciphers.", @@ -933,14 +1014,27 @@ "type": "Тип" }, "queue": { - "queue_manager": "Очередь на отправку" + "ays": "Пожалуйста, подтвердите, что вы хотите удалить все элементы из текущей очереди.", + "delete": "Удалить все", + "deliver_mail": "Доставить", + "deliver_mail_legend": "Попытаться повторно доставить выбранные письма.", + "flush": "Обработать очередь", + "hold_mail": "Отложить", + "hold_mail_legend": "Удержать выбранные сообщения. (Предотвратить дальнейшие попытки доставки)", + "info": "Очередь отправки почты содержит все письма, которые ожидают доставки. Если письмо надолго задерживается в почтовой очереди, оно автоматически удаляется системой.
Сообщение об ошибке в соответствующих письмах содержит информацию о том, почему письмо не удалось доставить.", + "legend": "Описание действий с почтовой очередью:", + "queue_manager": "Очередь на отправку", + "show_message": "Показать сообщение", + "unhold_mail": "Высвободить", + "unhold_mail_legend": "Освобождает выбранные письма для доставки. (Требуется предварительное удержание)", + "unban": "освободить очередь" }, "ratelimit": { + "day": "сообщений / день", "disabled": "Отключен", - "second": "сообщений / секунду", - "minute": "сообщений / минуту", "hour": "сообщений / час", - "day": "сообщений / день" + "minute": "сообщений / минуту", + "second": "сообщений / секунду" }, "start": { "help": "Справка", @@ -966,6 +1060,7 @@ "bcc_deleted": "Правила BCC удалены: %s", "bcc_edited": "Правило BCC %s отредактировано", "bcc_saved": "Правило BCC сохранено", + "cors_headers_edited": "Настройки CORS сохранены", "db_init_complete": "Инициализация базы данных завершена", "delete_filter": "Фильтр ID %s удалён", "delete_filters": "Фильтры удалены: %s", @@ -974,19 +1069,23 @@ "dkim_added": "DKIM ключ сохранён", "dkim_duplicated": "DKIM ключи для домена %s были скопированы в %s", "dkim_removed": "DKIM ключ %s удалён", + "domain_add_dkim_available": "DKIM ключ уже существует", "domain_added": "Добавлен домен %s", "domain_admin_added": "Администратор домена %s добавлен", "domain_admin_modified": "Сохранить изменения администратора домена %s", "domain_admin_removed": "Администратор домена %s удалён", + "domain_footer_modified": "Изменения в нижнем колонтитуле домена %s сохранены", "domain_modified": "Сохранить изменения домена %s", "domain_removed": "Домен %s удалён", "dovecot_restart_success": "Dovecot перезапущен успешно", "eas_reset": "Кеш ActiveSync для пользователя %s был сброшен", + "f2b_banlist_refreshed": "Идентификатор банлиста был успешно обновлен.", "f2b_modified": "Изменения параметров Fail2ban сохранены", "forwarding_host_added": "Перенаправление узла %s добавлено", "forwarding_host_removed": "Перенаправление узла %s удалено", "global_filter_written": "Фильтр успешно записан в файл", "hash_deleted": "Хеш удалён", + "ip_check_opt_in_modified": "Параметры проверки IP успешно обновлены", "item_deleted": "Обьект %s удалён", "item_released": "Письмо %s восстановлено из карантина", "items_deleted": "Обьекты %s удалены", @@ -997,14 +1096,17 @@ "mailbox_added": "Почтовый аккаунт %s добавлен", "mailbox_modified": "Изменения почтового аккаунта %s сохранены", "mailbox_removed": "Почтовый аккаунт %s удалён", + "mailbox_renamed": "Почтовый аккаунт %s был переименован в %s", "nginx_reloaded": "Обновление конфигурация Nginx закончено", "object_modified": "Изменения объекта %s сохранены", + "password_changed_success": "Пароль был успешно изменен", "password_policy_saved": "Политика паролей сохранена", "pushover_settings_edited": "Настройки сохранены, пожалуйста, выполните проверку доступа", "qlearn_spam": "Письмо ID %s было изучено как спам и удалено", "queue_command_success": "Команда выполнена успешно", "recipient_map_entry_deleted": "Правило перезаписи получателя ID %s было удалено", "recipient_map_entry_saved": "Правило перезаписи получателя \"%s\" было сохранено", + "recovery_email_sent": "Письмо для восстановления пароля отправлено на %s", "relayhost_added": "Промежуточный узел %s добавлен", "relayhost_removed": "Промежуточный узел %s удалён", "reset_main_logo": "Восстановить логотип по умолчанию", @@ -1017,6 +1119,9 @@ "settings_map_added": "Правило добавлено", "settings_map_removed": "Правило ID %s удалено", "sogo_profile_reset": "Профиль пользователя SOGo %s сброшен", + "template_added": "Шаблон %s добавлен", + "template_modified": "Изменения шаблона %s сохранены", + "template_removed": "Шаблон ID %s удален", "tls_policy_map_entry_deleted": "Политика TLS ID %s удалено", "tls_policy_map_entry_saved": "Политика TLS \"%s\" сохранена", "ui_texts": "Изменения текстов UI сохранены", @@ -1024,13 +1129,11 @@ "verified_fido2_login": "Авторизация FIDO2 пройдена", "verified_totp_login": "Авторизация TOTP пройдена", "verified_webauthn_login": "Авторизация WebAuthn пройдена", - "verified_yotp_login": "Авторизация Yubico OTP пройдена", - "cors_headers_edited": "Настройки CORS сохранены", - "domain_footer_modified": "Изменения в нижнем колонтитуле домена %s сохранены", - "f2b_banlist_refreshed": "Идентификатор банлиста был успешно обновлен." + "verified_yotp_login": "Авторизация Yubico OTP пройдена" }, "tfa": { "api_register": "%s использует Yubico Cloud API. Пожалуйста, получите ключ API для вашего ключа здесь", + "authenticators": "Аутентификаторы", "confirm": "Подтвердите", "confirm_totp_token": "Пожалуйста, подтвердите изменения, введя сгенерированный код", "delete_tfa": "Отключить TFA", @@ -1049,11 +1152,12 @@ "tfa": "Двухфакторная проверка подлинности", "tfa_token_invalid": "Неправильный TFA токен", "totp": "OTP (Authy, Google Authenticator и др.)", - "webauthn": "WebAuthn аутентификация", + "u2f_deprecated": "Похоже, что ваш ключ был зарегистрирован с использованием устаревшего метода U2F. Мы деактивируем для вас двухфакторную аутентификацию и удалим ваш ключ.", + "u2f_deprecated_important": "Пожалуйста, зарегистрируйте ваш ключ в панели администратора с помощью нового метода WebAuthn.", "waiting_usb_auth": "Ожидание устройства USB...

Пожалуйста, нажмите кнопку на USB устройстве сейчас.", "waiting_usb_register": "Ожидание устройства USB...

Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.", - "yubi_otp": "Yubico OTP аутентификация", - "u2f_deprecated": "Похоже, что ваш ключ был зарегистрирован с использованием устаревшего метода U2F. Мы деактивируем для вас двухфакторную аутентификацию и удалим ваш ключ." + "webauthn": "WebAuthn аутентификация", + "yubi_otp": "Yubico OTP аутентификация" }, "user": { "action": "Действия", @@ -1070,13 +1174,17 @@ "alias_valid_until": "Действителен до", "aliases_also_send_as": "Разрешено отправлять письма от имени", "aliases_send_as_all": "Разрешено отправлять письма от любого имени для домена и его псевдонимов", + "allowed_protocols": "Разрешенные протоколы", "app_hint": "Пароли приложений - это альтернативные пароли для авторизации в IMAP, SMTP, CalDAV, CardDAV и EAS. При этом имя пользователя остается неизменным.
SOGo недоступен через пароли приложений.", "app_name": "Название приложения", "app_passwds": "Пароли приложений", "apple_connection_profile": "Профиль подключения Apple", "apple_connection_profile_complete": "Этот профиль включает настройки IMAP и SMTP, а также CalDAV (календарей) и CardDAV (контактов) для устройства Apple.", "apple_connection_profile_mailonly": "Этот профиль включает только настройки IMAP и SMTP для устройства Apple.", + "apple_connection_profile_with_app_password": "Новый пароль приложения генерируется и добавляется в профиль, поэтому при настройке устройства не требуется вводить пароль. Не предоставляйте доступ к файлу, поскольку он предоставляет полный доступ к вашему почтовому ящику.", + "attribute": "Атрибут", "change_password": "Изменить пароль", + "change_password_hint_app_passwords": "В вашей учетной записи есть %d паролей приложений, которые не будут изменены. Чтобы управлять ими, перейдите на вкладку \"Пароли приложений\".", "clear_recent_successful_connections": "Очистить историю успешных подключений", "client_configuration": "Показать руководство по настройке почтовых клиентов и смартфонов", "create_app_passwd": "Создать новый пароль", @@ -1087,6 +1195,7 @@ "delete_ays": "Пожалуйста, подтвердите удаление", "direct_aliases": "Личные псевдонимы", "direct_aliases_desc": "На личные псевдонимы распространяются фильтры нежелательной почты и параметры политики TLS.", + "direct_protocol_access": "Этот пользователь почтового ящика имеет прямой, внешний доступ к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.
Кнопка \"Вход в веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.", "eas_reset": "Сбросить кеш ActiveSync устройств", "eas_reset_help": "Во многих случаях сброс кеша устройств помогает восстановить повреждённый профиль ActiveSync.
Внимание: все письма, календари и контакты будут загружены заново на все ваши устройства!", "eas_reset_now": "Сбросить кеш сейчас", @@ -1131,15 +1240,18 @@ "password": "Пароль", "password_now": "Текущий пароль (подтверждение изменения)", "password_repeat": "Подтверждение пароля (повтор)", + "password_reset_info": "If no email for password recovery is provided, this function cannot be used.", "pushover_evaluate_x_prio": "Установить высокий приоритет уведомлений для писем с высоким приоритетом [X-Priority: 1]", "pushover_info": "Настройки Push-уведомления будут применяться ко всей почте %s (за исключением спама), включая псевдонимы (личные, общие и тегированные).", "pushover_only_x_prio": "Получать уведомления только о письмах с высоким приоритетом [X-Priority: 1]", "pushover_sender_array": "Получать уведомления от списка адресов электронной почты (envelop-from, разделённые запятыми):", "pushover_sender_regex": "Получать уведомления от отправителей, удовлетворяющих regex-выражению:", + "pushover_sound": "Звук уведомления", "pushover_text": "Текст уведомления", "pushover_title": "Заголовок уведомления", "pushover_vars": "Когда фильтрация по отправителю не определена, уведомения будут доставлятся от всех отправителей.
Можно использовать обычный фильтр по отправителю и расширенный regex-фильтр, а также оба сразу.
Пожалуйста, ознакомьтесь с Pushover Privacy Policy перед использованием шаблонов для текста и заголовка", "pushover_verify": "Проверить доступ", + "pw_recovery_email": "Адрес для восстановления пароля", "q_add_header": "Нежелательная почта", "q_all": "Все категории", "q_reject": "Отклонённая почта", @@ -1180,15 +1292,15 @@ "spamfilter_yellow": "Жёлтый: эти письма могут быть спамом, будут доставлены в папку \"Спам\"", "status": "Статус", "sync_jobs": "Задания синхронизации", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", + "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", + "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", + "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", + "syncjob_EX_OK": "Успешно", "syncjob_check_log": "Проверить журнал", "syncjob_last_run_result": "Результат", - "syncjob_EX_OK": "Успешно", - "syncjob_EXIT_CONNECTION_FAILURE": "Ошибка связи с сервером", - "syncjob_EXIT_TLS_FAILURE": "Ошибка установки шифрованного соединения", - "syncjob_EXIT_AUTHENTICATION_FAILURE": "Ошибка авторизации", - "syncjob_EXIT_OVERQUOTA": "Целевой почтовый ящик превысил квоту", - "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не удалось подключиться к удаленному серверу", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Неправильное имя пользователя или пароль", "tag_handling": "Обработка тегированной почты", "tag_help_example": "Пример тегированного адреса электронной почты: ich+Facebook@example.org", "tag_help_explain": "Переместить в подпапку: будет создана новая подпапка в INBOX с именем тега, например: \"INBOX/Facebook\".
\r\n Добавить к теме письма: имя тега будет добавлено к теме письма, например: \"[Facebook] My News\".", @@ -1203,20 +1315,15 @@ "tls_policy_warning": "Предупреждение: Если вы включите принудительное шифрованние почты, вы можете столкнуться с потерей писем.
Сообщения, которые не соответствуют политике, будут отбрасываться с сообщением почтовым сервером о серьёзном сбое.
Этот параметр применяется к вашему основному адресу электронной почты (логину), всем личным псевдонимам и псевдонимам доменов. Подразумеваются только псевдонимы с одним почтовым ящиком, как получатель.", "user_settings": "Настройки пользователя", "username": "Имя пользователя", + "value": "Значение", "verify": "Проверить", "waiting": "В ожидании", "week": "неделю", "weekly": "Раз в неделю", "weeks": "недели", - "year": "год", - "years": "лет", - "allowed_protocols": "Разрешенные протоколы", - "apple_connection_profile_with_app_password": "Новый пароль приложения генерируется и добавляется в профиль, поэтому при настройке устройства не требуется вводить пароль. Не предоставляйте доступ к файлу, поскольку он предоставляет полный доступ к вашему почтовому ящику.", - "direct_protocol_access": "Этот пользователь почтового ящика имеет прямой, внешний доступ к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.
Кнопка \"веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.", "with_app_password": "с паролем приложения", - "change_password_hint_app_passwords": "В вашей учетной записи есть {{number_of_app_passwords}} паролей приложений, которые не будут изменены. Чтобы управлять ими, перейдите на вкладку \"Пароли приложений\".", - "attribute": "Атрибут", - "value": "Значение" + "year": "год", + "years": "лет" }, "warning": { "cannot_delete_self": "Вы не можете удалить сами себя", @@ -1230,10 +1337,5 @@ "quota_exceeded_scope": "Квота домена превышена: могут быть созданы только почтовые ящики без лимита.", "session_token": "Неверный токен формы: несоответствие токена", "session_ua": "Неверный токен формы: ошибка проверки User-Agent" - }, - "datatables": { - "collapse_all": "Свернуть все", - "expand_all": "Развернуть все", - "infoPostFix": "" } } diff --git a/data/web/mailbox.php b/data/web/mailbox.php index 65c76f531..a84e32c47 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -41,6 +41,7 @@ $template_data = [ 'mailboxes' => $mailboxes, 'lang_mailbox' => json_encode($lang['mailbox']), 'lang_rl' => json_encode($lang['ratelimit']), + 'lang_edit' => json_encode($lang['edit']), 'lang_datatables' => json_encode($lang['datatables']), ]; diff --git a/data/web/templates/admin/tab-config-f2b.twig b/data/web/templates/admin/tab-config-f2b.twig index bb4a2e85a..75c626641 100644 --- a/data/web/templates/admin/tab-config-f2b.twig +++ b/data/web/templates/admin/tab-config-f2b.twig @@ -99,7 +99,7 @@ {% endif %}
- + {% if is_https %} {% endif %} diff --git a/data/web/templates/mailbox.twig b/data/web/templates/mailbox.twig index b61896d70..f0b1af464 100644 --- a/data/web/templates/mailbox.twig +++ b/data/web/templates/mailbox.twig @@ -68,6 +68,7 @@ var acl = '{{ acl_json|raw }}'; var lang = {{ lang_mailbox|raw }}; var lang_rl = {{ lang_rl|raw }}; + var lang_edit = {{ lang_edit|raw }}; var lang_datatables = {{ lang_datatables|raw }}; var csrf_token = '{{ csrf_token }}'; var pagination_size = Math.trunc('{{ pagination_size }}'); diff --git a/docker-compose.yml b/docker-compose.yml index 85395b645..9218b3960 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,8 +43,10 @@ services: redis-mailcow: image: redis:7-alpine + entrypoint: /redis-conf.sh volumes: - redis-vol-1:/data/ + - ./data/conf/redis/redis-conf.sh:/redis-conf.sh:z restart: always depends_on: - netfilter-mailcow @@ -52,6 +54,7 @@ services: - "${REDIS_PORT:-127.0.0.1:7654}:6379" environment: - TZ=${TZ} + - REDISPASS=${REDISPASS} sysctls: - net.core.somaxconn=4096 networks: @@ -80,7 +83,7 @@ services: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.98 + image: mailcow/rspamd:1.99 stop_grace_period: 30s depends_on: - dovecot-mailcow @@ -91,6 +94,7 @@ services: - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-} volumes: - ./data/hooks/rspamd:/hooks:Z @@ -112,7 +116,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.91.1 + image: mailcow/phpfpm:1.92 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow @@ -147,6 +151,7 @@ services: environment: - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - LOG_LINES=${LOG_LINES:-9999} - TZ=${TZ} - DBNAME=${DBNAME} @@ -193,7 +198,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:nightly-20241112 + image: mailcow/sogo:nightly-20241205 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -210,6 +215,7 @@ services: - MASTER=${MASTER:-y} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} dns: - ${IPV4_NETWORK:-172.22.1}.254 volumes: @@ -240,7 +246,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:nightly-20241112 + image: mailcow/dovecot:nightly-20241205 depends_on: - mysql-mailcow - netfilter-mailcow @@ -282,6 +288,7 @@ services: - MASTER=${MASTER:-y} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} - FLATCURVE_EXPERIMENTAL=${FLATCURVE_EXPERIMENTAL:-n} ports: @@ -324,7 +331,7 @@ services: - dovecot postfix-mailcow: - image: mailcow/postfix:1.77 + image: mailcow/postfix:1.78 depends_on: mysql-mailcow: condition: service_started @@ -346,6 +353,7 @@ services: - DBPASS=${DBPASS} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-} cap_add: @@ -375,33 +383,26 @@ services: nginx-mailcow: depends_on: - - sogo-mailcow - - php-fpm-mailcow - redis-mailcow - image: nginx:mainline-alpine + - php-fpm-mailcow + - sogo-mailcow + - rspamd-mailcow + image: mailcow/nginx:1.00 dns: - ${IPV4_NETWORK:-172.22.1}.254 - command: /bin/sh -c "envsubst < /etc/nginx/conf.d/templates/listen_plain.template > /etc/nginx/conf.d/listen_plain.active && - envsubst < /etc/nginx/conf.d/templates/listen_ssl.template > /etc/nginx/conf.d/listen_ssl.active && - envsubst < /etc/nginx/conf.d/templates/sogo.template > /etc/nginx/conf.d/sogo.active && - . /etc/nginx/conf.d/templates/server_name.template.sh > /etc/nginx/conf.d/server_name.active && - . /etc/nginx/conf.d/templates/sites.template.sh > /etc/nginx/conf.d/sites.active && - . /etc/nginx/conf.d/templates/sogo_eas.template.sh > /etc/nginx/conf.d/sogo_eas.active && - nginx -qt && - until ping phpfpm -c1 > /dev/null; do sleep 1; done && - until ping sogo -c1 > /dev/null; do sleep 1; done && - until ping redis -c1 > /dev/null; do sleep 1; done && - until ping rspamd -c1 > /dev/null; do sleep 1; done && - exec nginx -g 'daemon off;'" environment: - HTTPS_PORT=${HTTPS_PORT:-443} - HTTP_PORT=${HTTP_PORT:-80} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-} - TZ=${TZ} - SKIP_SOGO=${SKIP_SOGO:-n} - - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} - - ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-} + - SKIP_RSPAMD=${SKIP_RSPAMD:-n} + - PHPFPMHOST=${PHPFPMHOST:-php-fpm-mailcow} + - SOGOHOST=${SOGOHOST:-sogo-mailcow} + - RSPAMDHOST=${RSPAMDHOST:-rspamd-mailcow} + - REDISHOST=${REDISHOST:-redis-mailcow} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} volumes: - ./data/web:/web:ro,z - ./data/conf/rspamd/dynmaps:/dynmaps:ro,z @@ -428,7 +429,7 @@ services: condition: service_started unbound-mailcow: condition: service_healthy - image: mailcow/acme:1.90 + image: mailcow/acme:1.91 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: @@ -451,6 +452,7 @@ services: - TZ=${TZ} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n} - SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n} volumes: @@ -465,7 +467,7 @@ services: - acme netfilter-mailcow: - image: mailcow/netfilter:1.59 + image: mailcow/netfilter:1.60 stop_grace_period: 30s restart: always privileged: true @@ -477,6 +479,7 @@ services: - SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-} - DISABLE_NETFILTER_ISOLATION_RULE=${DISABLE_NETFILTER_ISOLATION_RULE:-n} network_mode: "host" @@ -484,7 +487,7 @@ services: - /lib/modules:/lib/modules:ro watchdog-mailcow: - image: mailcow/watchdog:2.05 + image: mailcow/watchdog:2.06 dns: - ${IPV4_NETWORK:-172.22.1}.254 tmpfs: @@ -530,6 +533,7 @@ services: - HTTPS_PORT=${HTTPS_PORT:-443} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} - EXTERNAL_CHECKS_THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD:-1} - NGINX_THRESHOLD=${NGINX_THRESHOLD:-5} - UNBOUND_THRESHOLD=${UNBOUND_THRESHOLD:-5} @@ -555,7 +559,7 @@ services: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:2.09 + image: mailcow/dockerapi:2.10 security_opt: - label=disable restart: always @@ -566,6 +570,7 @@ services: - TZ=${TZ} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: diff --git a/generate_config.sh b/generate_config.sh index 1a9909a9f..35a0eddd3 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -26,7 +26,7 @@ for bin in openssl curl docker git awk sha1sum grep cut; do done # Check Docker Version (need at least 24.X) -docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1) +docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | head -n 1 | cut -d '.' -f 1) if [[ $docker_version -lt 24 ]]; then echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" @@ -43,7 +43,7 @@ if docker compose > /dev/null 2>&1; then sleep 2 echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" else - echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" exit 1 fi @@ -56,14 +56,14 @@ elif docker-compose > /dev/null 2>&1; then sleep 2 echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" else - echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" exit 1 fi fi else - echo -e "\e[31mCannot find Docker Compose.\e[0m" + echo -e "\e[31mCannot find Docker Compose.\e[0m" echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" exit 1 fi @@ -229,7 +229,7 @@ else echo -e "\033[31mCould not determine branch input..." echo -e "\033[31mExiting." exit 1 -fi +fi if [ ! -z "${MAILCOW_BRANCH}" ]; then git_branch=${MAILCOW_BRANCH} @@ -264,6 +264,12 @@ DBUSER=mailcow DBPASS=$(LC_ALL=C /dev/null | head -c 28) DBROOT=$(LC_ALL=C /dev/null | head -c 28) +# ------------------------------ +# REDIS configuration +# ------------------------------ + +REDISPASS=$(LC_ALL=C /dev/null | head -c 28) + # ------------------------------ # HTTP/S Bindings # ------------------------------ @@ -510,7 +516,7 @@ WEBAUTHN_ONLY_TRUSTED_VENDORS=n # Spamhaus Data Query Service Key # Optional: Leave empty for none -# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist. +# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist. # If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS. # Otherwise it will work normally. SPAMHAUS_DQS_KEY= diff --git a/helper-scripts/_cold-standby.sh b/helper-scripts/_cold-standby.sh index ff0512e07..815152735 100755 --- a/helper-scripts/_cold-standby.sh +++ b/helper-scripts/_cold-standby.sh @@ -150,7 +150,7 @@ else exit 1 fi - REMOTE_ARCH=$(ssh -o StrictHostKeyChecking=no -i "${REMOTE_SSH_KEY}" ${REMOTE_SSH_HOST} -p ${REMOTE_SSH_PORT} "uname -m") + REMOTE_ARCH=$(ssh -o StrictHostKeyChecking=no -i "${REMOTE_SSH_KEY}" ${REMOTE_SSH_HOST} -p ${REMOTE_SSH_PORT} "uname -m") } @@ -204,7 +204,7 @@ fi # Trigger a Redis save for a consistent Redis copy echo -ne "\033[1mRunning redis-cli save... \033[0m" -docker exec $(docker ps -qf name=redis-mailcow) redis-cli save +docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save # Syncing volumes related to compose project # Same here: make sure destination exists diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index dc30d5ea1..581a84091 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -119,7 +119,7 @@ function backup() { ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt ;;& redis|all) - docker exec $(docker ps -qf name=redis-mailcow) redis-cli save + docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save docker run --name mailcow-backup --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \ diff --git a/helper-scripts/nextcloud.sh b/helper-scripts/nextcloud.sh index 2df3ca161..12dab3ef2 100755 --- a/helper-scripts/nextcloud.sh +++ b/helper-scripts/nextcloud.sh @@ -101,11 +101,11 @@ if [[ ${NC_PURGE} == "y" ]]; then echo -e "\033[33mNot purging anything...\033[0m" exit 1 fi - docker exec -it $(docker ps -f name=redis-mailcow -q) /bin/sh -c ' cat <> mailcow.conf echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf fi + elif [[ "${option}" == "REDISPASS" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo -e '\n# ------------------------------' >> mailcow.conf + echo '# REDIS configuration' >> mailcow.conf + echo -e '# ------------------------------\n' >> mailcow.conf + echo "REDISPASS=$(LC_ALL=C /dev/null | head -c 28)" >> mailcow.conf + fi elif ! grep -q "${option}" mailcow.conf; then echo "Adding new option \"${option}\" to mailcow.conf" echo "${option}=n" >> mailcow.conf