Browse Source

Merge remote-tracking branch 'origin/staging' into nightly

FreddleSpl0it 9 months ago
parent
commit
a41bb55c83
69 changed files with 959 additions and 690 deletions
  1. 1 0
      .github/FUNDING.yml
  2. 2 2
      data/Dockerfiles/acme/acme.sh
  3. 1 1
      data/Dockerfiles/acme/obtain-certificate.sh
  4. 2 2
      data/Dockerfiles/dockerapi/main.py
  5. 1 1
      data/Dockerfiles/dovecot/clean_q_aged.sh
  6. 2 2
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  7. 1 1
      data/Dockerfiles/dovecot/quarantine_notify.py
  8. 1 1
      data/Dockerfiles/dovecot/quota_notify.py
  9. 2 2
      data/Dockerfiles/dovecot/repl_health.sh
  10. 2 0
      data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf
  11. 2 0
      data/Dockerfiles/dovecot/syslog-ng.conf
  12. 2 2
      data/Dockerfiles/dovecot/trim_logs.sh
  13. 4 4
      data/Dockerfiles/netfilter/main.py
  14. 18 0
      data/Dockerfiles/nginx/Dockerfile
  15. 76 0
      data/Dockerfiles/nginx/bootstrap.py
  16. 26 0
      data/Dockerfiles/nginx/docker-entrypoint.sh
  17. 2 2
      data/Dockerfiles/phpfpm/docker-entrypoint.sh
  18. 2 0
      data/Dockerfiles/postfix/syslog-ng-redis_slave.conf
  19. 2 0
      data/Dockerfiles/postfix/syslog-ng.conf
  20. 7 5
      data/Dockerfiles/rspamd/docker-entrypoint.sh
  21. 2 0
      data/Dockerfiles/sogo/syslog-ng-redis_slave.conf
  22. 2 0
      data/Dockerfiles/sogo/syslog-ng.conf
  23. 10 10
      data/Dockerfiles/watchdog/watchdog.sh
  24. 1 1
      data/conf/mysql/my.cnf
  25. 0 3
      data/conf/nginx/000-map-size.conf
  26. 0 19
      data/conf/nginx/dynmaps.conf
  27. 0 242
      data/conf/nginx/includes/site-defaults.conf
  28. 0 8
      data/conf/nginx/includes/sogo_proxy_auth.conf
  29. 0 23
      data/conf/nginx/mailcow_auth.conf
  30. 0 19
      data/conf/nginx/meta_exporter.conf
  31. 142 0
      data/conf/nginx/nginx.conf.j2
  32. 0 10
      data/conf/nginx/site.conf
  33. 276 0
      data/conf/nginx/sites-default.conf.j2
  34. 0 2
      data/conf/nginx/templates/listen_plain.template
  35. 0 3
      data/conf/nginx/templates/listen_ssl.template
  36. 0 1
      data/conf/nginx/templates/server_name.template.sh
  37. 0 38
      data/conf/nginx/templates/sites.template.sh
  38. 0 1
      data/conf/nginx/templates/sogo.template
  39. 0 5
      data/conf/nginx/templates/sogo_eas.template.sh
  40. 17 59
      data/conf/postfix/postscreen_access.cidr
  41. 7 0
      data/conf/redis/redis-conf.sh
  42. 1 0
      data/conf/rspamd/dynmaps/aliasexp.php
  43. 1 0
      data/conf/rspamd/dynmaps/forwardinghosts.php
  44. 4 3
      data/conf/rspamd/meta_exporter/pipe.php
  45. 1 0
      data/conf/rspamd/meta_exporter/pipe_rl.php
  46. 1 0
      data/conf/rspamd/meta_exporter/pushover.php
  47. 1 0
      data/web/_rspamderror.php
  48. 1 1
      data/web/admin.php
  49. 1 0
      data/web/autodiscover.php
  50. 11 0
      data/web/f2b-banlist.php
  51. 2 2
      data/web/inc/functions.dkim.inc.php
  52. 2 0
      data/web/inc/functions.inc.php
  53. 6 1
      data/web/inc/functions.mailbox.inc.php
  54. 12 8
      data/web/inc/init_db.inc.php
  55. 1 0
      data/web/inc/prerequisites.inc.php
  56. 13 28
      data/web/js/site/mailbox.js
  57. 0 14
      data/web/json_api.php
  58. 1 1
      data/web/lang/lang.fr-fr.json
  59. 218 116
      data/web/lang/lang.ru-ru.json
  60. 1 0
      data/web/mailbox.php
  61. 1 1
      data/web/templates/admin/tab-config-f2b.twig
  62. 1 0
      data/web/templates/mailbox.twig
  63. 32 27
      docker-compose.yml
  64. 12 6
      generate_config.sh
  65. 2 2
      helper-scripts/_cold-standby.sh
  66. 1 1
      helper-scripts/backup_and_restore.sh
  67. 2 2
      helper-scripts/nextcloud.sh
  68. 5 5
      helper-scripts/reset-learns.sh
  69. 12 3
      update.sh

+ 1 - 0
.github/FUNDING.yml

@@ -1 +1,2 @@
+github: mailcow
 custom: ["https://www.servercow.de/mailcow?lang=en#sal"]

+ 2 - 2
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

+ 1 - 1
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

+ 2 - 2
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')

+ 1 - 1
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"

+ 2 - 2
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

+ 1 - 1
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))

+ 1 - 1
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))

+ 2 - 2
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?

+ 2 - 0
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)")
   );
 };

+ 2 - 0
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)")
   );
 };

+ 2 - 2
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}"

+ 4 - 4
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)

+ 18 - 0
data/Dockerfiles/nginx/Dockerfile

@@ -0,0 +1,18 @@
+FROM nginx:alpine
+LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+
+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;"]

+ 76 - 0
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()

+ 26 - 0
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 "$@"

+ 2 - 2
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)

+ 2 - 0
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)")
   );
 };

+ 2 - 0
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)")
   );
 };

+ 7 - 5
data/Dockerfiles/rspamd/docker-entrypoint.sh

@@ -56,27 +56,29 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
   cat <<EOF > /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 <<EOF > /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

+ 2 - 0
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)")
   );
 };

+ 2 - 0
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)")
   );
 };

+ 10 - 10
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

+ 1 - 1
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

+ 0 - 3
data/conf/nginx/000-map-size.conf

@@ -1,3 +0,0 @@
-map_hash_max_size 256;
-map_hash_bucket_size 256;
-

+ 0 - 19
data/conf/nginx/dynmaps.conf

@@ -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;
-  }
-}

+ 0 - 242
data/conf/nginx/includes/site-defaults.conf

@@ -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;
-  }

+ 0 - 8
data/conf/nginx/includes/sogo_proxy_auth.conf

@@ -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";
-

+ 0 - 23
data/conf/nginx/mailcow_auth.conf

@@ -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;
-  }
-}

+ 0 - 19
data/conf/nginx/meta_exporter.conf

@@ -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;
-  }
-}

+ 142 - 0
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 %}
+}

+ 0 - 10
data/conf/nginx/site.conf

@@ -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;

+ 276 - 0
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;
+}

+ 0 - 2
data/conf/nginx/templates/listen_plain.template

@@ -1,2 +0,0 @@
-listen ${HTTP_PORT};
-listen [::]:${HTTP_PORT};

+ 0 - 3
data/conf/nginx/templates/listen_ssl.template

@@ -1,3 +0,0 @@
-listen ${HTTPS_PORT} ssl;
-listen [::]:${HTTPS_PORT} ssl;
-http2 on;

+ 0 - 1
data/conf/nginx/templates/server_name.template.sh

@@ -1 +0,0 @@
-echo "server_name ${MAILCOW_HOSTNAME} autodiscover.* autoconfig.* $(echo ${ADDITIONAL_SERVER_NAMES} | tr ',' ' ');"

+ 0 - 38
data/conf/nginx/templates/sites.template.sh

@@ -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

+ 0 - 1
data/conf/nginx/templates/sogo.template

@@ -1 +0,0 @@
-proxy_pass http://${IPV4_NETWORK}.248:20000;

+ 0 - 5
data/conf/nginx/templates/sogo_eas.template.sh

@@ -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

+ 17 - 59
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

+ 7 - 0
data/conf/redis/redis-conf.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+cat <<EOF > /redis.conf
+requirepass $REDISPASS
+EOF
+
+exec redis-server /redis.conf

+ 1 - 0
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;

+ 1 - 0
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);

+ 4 - 3
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,

+ 1 - 0
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;

+ 1 - 0
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) {

+ 1 - 0
data/web/_rspamderror.php

@@ -7,6 +7,7 @@ try {
   else {
     $redis->connect('redis-mailcow', 6379);
   }
+  $redis->auth(getenv("REDISPASS"));
 }
 catch (Exception $e) {
   exit;

+ 1 - 1
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'),

+ 1 - 0
data/web/autodiscover.php

@@ -19,6 +19,7 @@ try {
   else {
     $redis->connect('redis-mailcow', 6379);
   }
+  $redis->auth(getenv("REDISPASS"));
 }
 catch (Exception $e) {
   exit;

+ 11 - 0
data/web/f2b-banlist.php

@@ -0,0 +1,11 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+
+if (isset($_GET['id'])) {
+    header('Content-Type: text/plain');
+    echo fail2ban('banlist', 'get', $_GET['id']);
+} else {
+    header('HTTP/1.1 404 Not Found');
+    exit;
+}

+ 2 - 2
data/web/inc/functions.dkim.inc.php

@@ -26,7 +26,7 @@ function dkim($_action, $_data = null, $privkey = false) {
           );
           continue;
         }
-        if (!ctype_alnum(str_replace(['-', '_'], '', $dkim_selector))) {
+        if (!ctype_alnum(str_replace(['-', '_', '.'], '', $dkim_selector))) {
           $_SESSION['return'][] = array(
             'type' => '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),

+ 2 - 0
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),

+ 6 - 1
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(

+ 12 - 8
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");

+ 1 - 0
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

+ 13 - 28
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 = '<div class="btn-group">' +
@@ -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 ? '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">IMAP @ ' + unix_time_format(Number(item.last_imap_login)) + '</div><br>' : '') +
+                                   (item.attributes.pop3_access == 1 ? '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">POP3 @ ' + unix_time_format(Number(item.last_pop3_login)) + '</div><br>' : '') +
+                                   (item.attributes.smtp_access == 1 ? '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">SMTP @ ' + unix_time_format(Number(item.last_smtp_login)) + '</div><br>' : '') +
+                                   '<div class="text-start badge bg-info" style="min-width: 70px;">SSO @ ' + unix_time_format(Number(item.last_sso_login)) + '</div>';
             /*
             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 '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">IMAP @ ' + unix_time_format(Number(res[0])) + '</div><br>' +
-              '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">POP3 @ ' + unix_time_format(Number(res[1])) + '</div><br>' +
-              '<div class="text-start badge bg-info mb-2" style="min-width: 70px;">SMTP @ ' + unix_time_format(Number(res[2])) + '</div><br>' +
-              '<div class="text-start badge bg-info" style="min-width: 70px;">SSO @ ' + unix_time_format(Number(res[3])) + '</div>';
-          }
+          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',
         },

+ 0 - 14
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);

+ 1 - 1
data/web/lang/lang.fr-fr.json

@@ -207,7 +207,7 @@
         "include_exclude_info": "Par défaut - sans sélection - <b>toutes les boîte de réception</b> 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.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Enregistrer votre GUID ici</a> or <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">acheter le support pour votre intallation Mailcow.</a>",
+        "license_info": "Une licence n’est pas requise, mais contribue au développement.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Enregistrer votre GUID ici</a> ou <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">acheter le support pour votre installation Mailcow.</a>",
         "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é.",

+ 218 - 116
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": "<small>Действительные имена доменов, раздёленные запятыми.</small>",
         "app_name": "Название приложения",
+        "app_passwd_protocols": "Разрешенные протоколы для пароля приложения",
         "app_password": "Добавить пароль приложения",
         "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
         "backup_mx_options": "Параметры резервного MX",
         "bcc_dest_format": "Место назначения BCC должно быть единственным действительным адресом электронной почты.<br>Если вам нужно отправить копию на несколько адресов - используйте псевдоним.",
         "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": "↪<small>Если вы решите <b>не</b> ретранслировать всех получателей, вам нужно будет добавить (\"слепой\") почтовый адрес для каждого получателя, которого следует ретранслировать.</small>",
         "relay_domain": "Ретрансляция этого домена",
         "relay_transport_info": "<div class=\"badge fs-6 bg-info\">Инфо</div> Вы можете настроить собственный транспорт для домена. Если такой настройки нет, то доставка будет выполнена на основе MX записей.",
-        "relay_unknown_only": "Ретрансляция только не существующих почтовых ящиков. Почта к существующим почтовым ящикам будут доставляться локально.",
+        "relay_unknown_only": "Ретрансляция только несуществующих почтовых ящиков. Почта существующих почтовых ящиков будут доставляться локально.",
         "relayhost_wrapped_tls_info": "Пожалуйста <b>не</b> используйте TLS порты (в основном это 465 порт).<br>\r\nИспользуйте любой <b>не</b> TLS порт который поддерживает STARTTLS. А для защиты от downgrate атак - настройке принудительную политику TLS.",
         "select": "Пожалуйста, выберите...",
         "select_domain": "Пожалуйста, сначала выберите домен",
@@ -99,6 +101,7 @@
         "subscribeall": "Подписаться на все папки и подпапки",
         "syncjob": "Добавить задание синхронизации",
         "syncjob_hint": "Пароли к вашему аккаунту будут сохранены на сервере в виде простого текста!",
+        "tags": "Теги",
         "target_address": "Владельцы псевдонима",
         "target_address_info": "<small>Адреса почтовых ящиков, разделенные запятыми.</small>",
         "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 находится в стадии разработки. Документация находится по адресу <a href=\"/api\">/api</a>",
         "api_key": "Ключ API",
@@ -151,6 +153,8 @@
         "change_logo": "Изменить логотип",
         "configuration": "Глобальные настройки",
         "convert_html_to_text": "Сконвертировать HTML в обычный текст",
+        "copy_to_clipboard": "Текст скопирован в буфер обмена!",
+        "cors_settings": "Настройки CORS",
         "credentials_transport_warning": "<b>Предупреждение</b>: добавление новой записи перезапишет учетные данные для всех записей с таким же <i>следующим хостом</i>.",
         "customer_id": "ID клиента",
         "customize": "Персонализация",
@@ -179,10 +183,14 @@
         "empty": "Пусто",
         "excludes": "Исключает этих получателей",
         "f2b_ban_time": "Время бана (в секундах)",
+        "f2b_ban_time_increment": "Время бана увеличивается с каждым баном",
         "f2b_blacklist": "Черный список подсетей/хостов",
         "f2b_filter": "Правила фильтрации с помощью регулярных выражений",
         "f2b_list_info": "Хосты или подсети, занесенные в черный список, всегда будут перевешивать объекты из белого списка. <b>Обновление списка займет несколько секунд.</b>",
+        "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": "По умолчанию - без выбора - <b>все почтовые ящики</b> адресованы",
         "includes": "Включить этих получателей",
+        "ip_check": "Проверить IP",
+        "ip_check_disabled": "Проверка IP-адресов отключена. Вы можете включить её в разделе <br> <strong>Система > Конфигурация > Параметры > Персонализация</strong>.",
+        "ip_check_opt_in": "Согласие на использование сторонних служб <strong>ipv4.mailcow.email</strong> и <strong>ipv6.mailcow.email</strong> для разрешения внешних IP-адресов.",
         "is_mx_based": "На основе MX",
         "last_applied": "Посл. применение",
-        "license_info": "Лицензия не обязательна, но её приобретение помогает дальнейшему развитию mailcow.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Зарегистрируйте свой GUID здесь</a> или <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Заказать поддержку\">приобретите поддержку для вашей установки mailcow.</a>",
+        "license_info": "Лицензия необязательна, но её приобретение помогает дальнейшему развитию mailcow.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Зарегистрируйте свой GUID здесь</a> или <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Заказать поддержку\">приобретите поддержку для вашей установки mailcow.</a>",
         "link": "Ссылка",
         "loading": "Пожалуйста, подождите...",
         "login_time": "Время входа",
-        "logo_info": "Ваше изображение будет масштабироваться до высоты 40px для верхней панели навигации и до 250px ширины для стартовой страницы. <br>Рекомендуется использовать векторную графику, на пример: .svg.",
-        "lookup_mx": "Назначение на основе резовинга MX записи по регулярному выражению (<code>.*\\.example\\.com$</code> для маршрутизации всей почты через этот хост, если MX заканчивающийся на example.com)",
+        "logo_dark_label": "Инвертированный для темного режима",
+        "logo_info": "Ваше изображение будет масштабироваться до высоты 40px для верхней панели навигации и до 250px ширины для стартовой страницы. <br>Рекомендуется использовать векторную графику, например: .svg.",
+        "logo_normal_label": "Обычный",
+        "lookup_mx": "Назначение - регулярное выражение для сопоставления с именем MX (<code>.*\\.google\\.com</code> для направления всей почты, адресованной MX, заканчивающейся на google.com, через этот хоп)",
         "main_name": "Название для \"mailcow UI\"",
         "merged_vars_hint": "Серым цветом выделены строки полученные из <code>vars.(local.)inc.php</code>, они не могут быть изменены.",
         "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 поддерживает предоставления кодов авторизации и выдает токены продления сессии.<br>\r\nСервер также автоматически выдает новый токен продления сессии, после того, как предыдущий был использован.<br><br>\r\n&#8226; Scope по умолчанию: <i>profile</i>. Только пользователи почтовых аккаунтов могут проходить аутентификацию через OAuth2. Если параметр области не указан, он возвращается к <i>profile</i>.<br>\r\n&#8226; Параметр <i>state</i> должен быть отправлен клиентом как часть запроса для авторизации.<br><br>\r\nПути для запросов OAuth2 API: <br>\r\n<ul>\r\n  <li>Authorization endpoint: <code>/oauth/authorize</code></li>\r\n  <li>Token endpoint: <code>/oauth/token</code></li>\r\n  <li>Resource page:  <code>/oauth/profile</code></li>\r\n</ul>\r\nГенерирование нового клиентского секрета не приводит к истечению существующих кодов авторизации, но они не смогут обновить свой токен.<br><br>\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": "Максимальный период хранения в днях<br><small>Значение должно быть равно или больше 1 дня.</small>",
         "quarantine_max_score": "Не уведомлять о спаме, если оценка письма выше, чем:<br><small>По умолчанию 9999.0</small>",
-        "quarantine_max_size": "Максимальный размер в MiB (письма большего размера не будет сохранены):<br><small>0 означает, что карантин <b>отключён</b>.</small>",
+        "quarantine_max_size": "Максимальный размер в MiB (письма большего размера не будут сохранены):<br><small>0 означает, что карантин <b>отключён</b>.</small>",
         "quarantine_notification_html": "Шаблон уведомления:<br><small>Оставьте пустым, чтобы восстановить шаблон по умолчанию.</small>",
         "quarantine_notification_sender": "Email-адрес для отправки уведомления",
         "quarantine_notification_subject": "Тема письма",
@@ -259,6 +278,7 @@
         "quarantine_release_format_att": "Как вложение",
         "quarantine_release_format_raw": "Оригинальное письмо",
         "quarantine_retention_size": "Количество писем, сохраняемых в карантине на аккаунт:<br><small>0 означает, что карантин <b>отключён</b>.</small>",
+        "queue_unban": "разблокировать",
         "quota_notification_html": "Шаблон уведомления:<br><small>Оставьте пустым, чтобы восстановить шаблон по умолчанию.</small>",
         "quota_notification_sender": "Email-адрес для отправки уведомления",
         "quota_notification_subject": "Тема письма",
@@ -267,7 +287,7 @@
         "quota_notifications_vars": "{{percent}} равно текущей квоте пользователя<br>{{username}} - имя почтового аккаунта",
         "r_active": "Включенные ограничения",
         "r_inactive": "Отключенные ограничения",
-        "r_info": "Не активные (серые) элементы списка ограничений - это не валидные ограничения, и они не могут быть перемещены. <br>Вы можете добавить новые элементы в <code>inc/vars.local.inc.php</code> чтобы иметь возможность настраивать их.",
+        "r_info": "Неактивные (серые) элементы списка ограничений - это некорректные ограничения, и они не могут быть перемещены. <br>Вы можете добавить новые элементы в <code>inc/vars.local.inc.php</code> чтобы иметь возможность настраивать их.",
         "rate_name": "Название очереди",
         "recipients": "Получатели",
         "refresh": "Обновить",
@@ -282,6 +302,8 @@
         "remove_row": "Удалить строку",
         "reset_default": "Восстановить по умолчанию",
         "reset_limit": "Удалить хэш",
+        "reset_password_vars": "<code>{{link}}</code> Сгенерированная ссылка для восстановление пароля<br><code>{{username}}</code> Имя почтового ящика пользователя, запросившего восстановление пароля<br><code>{{username2}}</code> Имя почтового ящика для восстановления<br><code>{{date}}</code> Дата запроса на восстановление пароля<br><code>{{token_lifetime}}</code> Срок действия токена в минутах<br><code>{{hostname}}</code> Имя хоста 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.<br> Инструкция доступна на сайте <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">документация Rspamd user settings</a>, заготовленные шаблоны:",
         "rspamd_global_filters": "Глобальные правила фильтрации",
         "rspamd_global_filters_agree": "Я понимаю, что я делаю, и буду осторожен!",
         "rspamd_global_filters_info": "Глобальные правила фильтрации содержат различные виды глобальных черных и белых списков.",
-        "rspamd_global_filters_regex": "Названия фильтров отражают их предназначение. Все правила должены состоять из регулярных выражений в формате \"/pattern/options\" (на пример: <code>/.+@domain\\.tld/i</code>).<br>\r\nНесмотря на то, что перед сохранением правил выполняется проверка регулярных выражений, функциональность Rspamds может быть нарушена, если будет использован<br>\r\n некорректный синтаксис. Будьте внимательны при написании правил.<br>Электронные письма от адресов электронной почты, проходящие по регулярным выражениям черных списков, будут отклонены без сохранения в карантин.<br>\r\n Rspamd попытается прочитать содержимое правил при их изменении. Но, если что, вы можете <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">перезапустить Rspamd</a>, чтобы принять последние изменения принудительно.",
+        "rspamd_global_filters_regex": "Названия фильтров отражают их предназначение. Все правила должены состоять из регулярных выражений в формате \"/pattern/options\" (например: <code>/.+@domain\\.tld/i</code>).<br>\r\nНесмотря на то, что перед сохранением правил выполняется проверка регулярных выражений, функциональность Rspamds может быть нарушена, если будет использован<br>\r\n некорректный синтаксис. Будьте внимательны при написании правил.<br>Электронные письма от адресов электронной почты, проходящие по регулярным выражениям черных списков, будут отклонены без сохранения в карантин.<br>\r\n Rspamd попытается прочитать содержимое правил при их изменении. Но, если что, вы можете <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">перезапустить Rspamd</a>, чтобы принять последние изменения принудительно.",
         "rspamd_settings_map": "Правила Rspamd",
         "sal_level": "Уровень Муу",
         "save": "Сохранить изменения",
@@ -337,19 +359,7 @@
         "username": "Имя пользователя",
         "validate_license_now": "Получить лицензию на основе GUID с сервера лицензий",
         "verify": "Проверить",
-        "yes": "&#10003;",
-        "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 отключена. Вы можете включить его в разделе <br> <strong>Система > Конфигурация > Параметры > Настроить</strong>.",
-        "ip_check_opt_in": "Согласие на использование сторонних служб <strong>ipv4.mailcow.email</strong> и <strong>ipv6.mailcow.email</strong> для разрешения внешних IP-адресов.",
-        "f2b_manage_external": "Внешнее управление Fail2Ban",
-        "f2b_manage_external_info": "Fail2ban по-прежнему будет вести банлист, но не будет активно устанавливать правила для блокировки трафика. Используйте сгенерированный ниже банлист для внешнего блокирования трафика.",
-        "copy_to_clipboard": "Текст скопирован в буфер обмена!"
+        "yes": "&#10003;"
     },
     "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": "<p><b>Журналы контейнеров</b> mailcow сохраняются в Redis, и раз в минуту строки журнала за пределами <code>LOG_LINES (%d)</code> удаляются, чтобы уменьшить нагрузку на сервер.\r\n  <br>Сами журналы контейнеров не сохраняются после перезагрузки контейнера. Все контейнеры дополнительно пишут логи в службу Docker, и, следовательно, используют драйвер логирования по умолчанию. Журналы контейнеров предусмотрены только для отладки мелких проблем. Для других задач, пожалуйста, настройте драйвер логирования Docker самостоятельно.</p>\r\n  <p><b>Внешние журналы</b> собираются через API приложений.</p>\r\n  <p><b>Статические журналы</b> &ndash; это, в основном, журналы активности, которые не записываются в Dockerd, но все равно должны быть постоянными (за исключением журналов API).</p>",
         "login_time": "Время входа",
         "logs": "Журналы",
+        "memory": "Память",
+        "no_update_available": "Система обновлена до последней версии",
         "online_users": "Подключено пользователей",
         "restart_container": "Перезапустить",
         "service": "Сервис",
+        "show_ip": "Показать публичные IP-адреса",
         "size": "Индексы занимают",
         "solr_dead": "Solr не запущен. Если вы включили Solf в файле настроек <code>mailcow.conf</code> и это сообщение отображается более получаса, скорее всего 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": "Нижние колонтитулы на уровне домена добавляются ко всем исходящим электронным письмам, связанным с адресом в этом домене. <br> Для нижнего колонтитула можно использовать следующие переменные:",
+        "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 &lt;moo@mailcow.tld&gt;\" возвращается \"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.<br>\r\n  Не забудьте добавить этот сервер к соответствующей записи SPF TXT внешнего домена.<br>\r\n  Добавление домена из списка внешних адресов в mailcow автоматически удалит соответствующие записи из внешних адресов пользователей.<br>\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 будет отсутствовать для домена, если данная функция будет отключена! <b>Требуется перезапустить SOGo, чтобы применить изменения.</b>",
@@ -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": "Получать уведомления только об письмах с высоким приоритетом [<code>X-Priority: 1</code>]",
         "pushover_sender_array": "Получать уведомления от списка адресов электронной почты <small>(envelop-from разделенные запятыми)</small>:",
         "pushover_sender_regex": "Получать уведомления от отправителей, удовлетворяющих regex-выражению:",
+        "pushover_sound": "Звук уведомления",
         "pushover_text": "Текст уведомления",
         "pushover_title": "Заголовок уведомления",
         "pushover_vars": "Когда фильтрация по отправителю не определена, уведомления будут доставлятся от всех отправителей.<br>Можно использовать обычный фильтр по отправителю и расширенный regex-фильтр, а также оба сразу.<br>Пожалуйста, ознакомьтесь с <a href=\"https://pushover.net/privacy\">Pushover Privacy Policy</a> перед использованием шаблонов для текста и заголовка",
@@ -600,7 +682,7 @@
         "relay_all_info": "↪<small>Если вы решите <b>не</b> ретранслировать всех получателей, вам нужно будет добавить (\"слепой\") почтовый аккаунт для каждого получателя, которого следует ретранслировать.</small>",
         "relay_domain": "Ретрансляция этого домена",
         "relay_transport_info": "<div class=\"badge fs-6 bg-info\">Инфо</div> Вы можете настроить собственный транспорт для домена. Если такой настройки нет, то доставка будет выполнена на основе 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": "Нижние колонтитулы на уровне домена добавляются ко всем исходящим электронным письмам, связанным с адресом в этом домене. <br> Для нижнего колонтитула можно использовать следующие переменные:",
-        "domain_footer_info_vars": {
-            "from_name": "{= from_name =} - Из названия envelope, например, для \"Mailcow &lt;moo@mailcow.tld&gt;\" возвращается \"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": "Токен восстановления пароля недействителен или срок его действия истек.<br>Пожалуйста, запросите новую ссылку для восстановления пароля.",
         "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": "Вы можете сохранить несколько фильтров для каждого пользователя, но только один предварительный фильтр и один постфильтр могут быть активными одновременно.<br>\r\n Каждый фильтр будет обработан в описанном порядке. Не сломанный скрипт, не <code>keep;</code> не остановит обработку дальнейших скриптов.<br><br>Global sieve prefilter &#8226; Prefilter &#8226; User scripts &#8226; Postfilter &#8226; Global sieve postfilter",
+        "sieve_info": "Вы можете сохранить несколько фильтров для каждого пользователя, но только один предварительный фильтр и один постфильтр могут быть активными одновременно.<br>\r\n Каждый фильтр будет обработан в описанном порядке. Ни сломанный скрипт, ни <code>keep;</code> не остановит обработку дальнейших скриптов.<br><br>Global sieve prefilter &#8226; Prefilter &#8226; User scripts &#8226; Postfilter &#8226; 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": "Пожалуйста, ознакомьтесь с примерами ниже. Для более подробной информации прочитайте <a href=\"https://en.wikipedia.org/wiki/Sieve_(mail_filtering_language)\" target=\"_blank\">Sieve Wikipedia</a>.",
         "sogo_visible": "Отображать псевдоним в SOGo",
         "sogo_visible_n": "Не отображать псевдоним в SOGo",
@@ -840,25 +919,27 @@
         "stats": "Статистика",
         "status": "Статус",
         "sync_jobs": "Задания синхронизации",
-        "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": "Неправильное имя пользователя или пароль",
+        "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": "Результат последнего запуска",
         "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": "Для исходящих сообщений от пользователей с включенной принудительной политикой шифрования исходящих соединений не описанные глобальной политикой,<br>\r\n будут применены значения по умолчанию, указанные в <code>smtp_tls_mandatory_protocols</code> и <code>smtp_tls_mandatory_ciphers</code>.",
@@ -933,14 +1014,27 @@
         "type": "Тип"
     },
     "queue": {
-        "queue_manager": "Очередь на отправку"
+        "ays": "Пожалуйста, подтвердите, что вы хотите удалить все элементы из текущей очереди.",
+        "delete": "Удалить все",
+        "deliver_mail": "Доставить",
+        "deliver_mail_legend": "Попытаться повторно доставить выбранные письма.",
+        "flush": "Обработать очередь",
+        "hold_mail": "Отложить",
+        "hold_mail_legend": "Удержать выбранные сообщения. (Предотвратить дальнейшие попытки доставки)",
+        "info": "Очередь отправки почты содержит все письма, которые ожидают доставки. Если письмо надолго задерживается в почтовой очереди, оно автоматически удаляется системой.<br>Сообщение об ошибке в соответствующих письмах содержит информацию о том, почему письмо не удалось доставить.",
+        "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 для вашего ключа <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">здесь</a>",
+        "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": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.",
         "waiting_usb_register": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на 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. При этом имя пользователя остается неизменным. <br>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": "Этот пользователь почтового ящика имеет <b>прямой, внешний доступ</b> к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.<br> Кнопка \"Вход в веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.",
         "eas_reset": "Сбросить кеш ActiveSync устройств",
         "eas_reset_help": "Во многих случаях сброс кеша устройств помогает восстановить повреждённый профиль ActiveSync.<br><b>Внимание:</b> все письма, календари и контакты будут загружены заново на все ваши устройства!",
         "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": "Установить высокий приоритет уведомлений для писем с высоким приоритетом [<code>X-Priority: 1</code>]",
         "pushover_info": "Настройки Push-уведомления будут применяться ко всей почте <b>%s</b> (за исключением спама), включая псевдонимы (личные, общие и тегированные).",
         "pushover_only_x_prio": "Получать уведомления только о письмах с высоким приоритетом [<code>X-Priority: 1</code>]",
         "pushover_sender_array": "Получать уведомления от списка адресов электронной почты <small>(envelop-from, разделённые запятыми)</small>:",
         "pushover_sender_regex": "Получать уведомления от отправителей, удовлетворяющих regex-выражению:",
+        "pushover_sound": "Звук уведомления",
         "pushover_text": "Текст уведомления",
         "pushover_title": "Заголовок уведомления",
         "pushover_vars": "Когда фильтрация по отправителю не определена, уведомения будут доставлятся от всех отправителей.<br>Можно использовать обычный фильтр по отправителю и расширенный regex-фильтр, а также оба сразу.<br>Пожалуйста, ознакомьтесь с <a href=\"https://pushover.net/privacy\">Pushover Privacy Policy</a> перед использованием шаблонов для текста и заголовка",
         "pushover_verify": "Проверить доступ",
+        "pw_recovery_email": "Адрес для восстановления пароля",
         "q_add_header": "Нежелательная почта",
         "q_all": "Все категории",
         "q_reject": "Отклонённая почта",
@@ -1180,15 +1292,15 @@
         "spamfilter_yellow": "Жёлтый: эти письма могут быть спамом, будут доставлены в папку \"Спам\"",
         "status": "Статус",
         "sync_jobs": "Задания синхронизации",
-        "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": "Неправильное имя пользователя или пароль",
+        "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": "Результат",
         "tag_handling": "Обработка тегированной почты",
         "tag_help_example": "Пример тегированного адреса электронной почты: <code>ich<b>+Facebook</b>@example.org</code>",
         "tag_help_explain": "Переместить в подпапку: будет создана новая подпапка в INBOX с именем тега, например: \"INBOX/Facebook\".<br>\r\n  Добавить к теме письма: имя тега будет добавлено к теме письма, например: \"[Facebook] My News\".",
@@ -1203,20 +1315,15 @@
         "tls_policy_warning": "<strong>Предупреждение:</strong> Если вы включите принудительное шифрованние почты, вы можете столкнуться с потерей писем.<br>Сообщения, которые не соответствуют политике, будут отбрасываться с сообщением почтовым сервером о серьёзном сбое.<br>Этот параметр применяется к вашему основному адресу электронной почты (логину), всем личным псевдонимам и псевдонимам доменов. Подразумеваются только псевдонимы <b>с одним почтовым ящиком</b>, как получатель.",
         "user_settings": "Настройки пользователя",
         "username": "Имя пользователя",
+        "value": "Значение",
         "verify": "Проверить",
         "waiting": "В ожидании",
         "week": "неделю",
         "weekly": "Раз в неделю",
         "weeks": "недели",
-        "year": "год",
-        "years": "лет",
-        "allowed_protocols": "Разрешенные протоколы",
-        "apple_connection_profile_with_app_password": "Новый пароль приложения генерируется и добавляется в профиль, поэтому при настройке устройства не требуется вводить пароль. Не предоставляйте доступ к файлу, поскольку он предоставляет полный доступ к вашему почтовому ящику.",
-        "direct_protocol_access": "Этот пользователь почтового ящика имеет <b>прямой, внешний доступ</b> к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.<br> Кнопка \"веб-почту\" обеспечивает единый вход в 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": ""
     }
 }

+ 1 - 0
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']),
 ];
 

+ 1 - 1
data/web/templates/admin/tab-config-f2b.twig

@@ -99,7 +99,7 @@
       {% endif %}
       <form class="form-inline" data-id="f2b_banlist" role="form" method="post">
         <div class="input-group mb-3">
-          <input type="text" class="form-control" aria-label="Banlist url" value="{{ f2b_banlist_url}}" id="banlist_url">
+          <input type="text" class="form-control" aria-label="Banlist url" value="{{ f2b_banlist_url }}" id="banlist_url">
           {% if is_https %}
           <button class="btn btn-secondary" type="button" onclick="copyToClipboard('banlist_url')"><i class="bi bi-clipboard"></i></button>
           {% endif %}

+ 1 - 0
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 }}');

+ 32 - 27
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:

+ 12 - 6
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/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28)
 DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28)
 
+# ------------------------------
+# REDIS configuration
+# ------------------------------
+
+REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /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=

+ 2 - 2
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

+ 1 - 1
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 \

+ 2 - 2
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 <<EOF | redis-cli
+  docker exec -it $(docker ps -f name=redis-mailcow -q) /bin/sh -c "cat <<EOF | redis-cli -a ${REDISPASS} --no-auth-warning
 SELECT 10
 FLUSHDB
 EOF
-'
+"
   if [ -d ./data/web/nextcloud/config ]; then
     mv ./data/web/nextcloud/config/ ./data/conf/nextcloud-config-folder-$(date +%s).bak
   fi

+ 5 - 5
helper-scripts/reset-learns.sh

@@ -15,15 +15,15 @@ if [[ "$response" =~ ^(yes|y)$ ]]; then
     docker stop ${RSPAMD_ID}
     echo "LUA will return nil when it succeeds or print a warning/error when it fails."
     echo "Deleting all RS* keys - if any"
-    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'RS*'
+    docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'RS*'
     echo "Deleting all BAYES* keys - if any"
-    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'BAYES*'
+    docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'BAYES*'
     echo "Deleting all learned* keys - if any"
-    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'learned*'
+    docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'learned*'
     echo "Deleting all fuzzy* keys - if any"
-    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'fuzzy*'
+    docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'fuzzy*'
     echo "Deleting all tRFANN* keys - if any"
-    docker exec -it ${REDIS_ID} redis-cli EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'tRFANN*'
+    docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'tRFANN*'
     echo "Starting Rspamd container"
     docker start ${RSPAMD_ID}
   fi

+ 12 - 3
update.sh

@@ -288,9 +288,9 @@ fix_broken_dnslist_conf() {
   # Check if the file contains the autogenerated comment
   if grep -q "# Autogenerated by mailcow" "$file"; then
       # Ask the user if custom changes were made
-      echo -e "\e[91mWARNING!!! \e[31mAn old version of dns_blocklists.cnf has been detected which may cause a broken postfix upon startup (see: https://github.com/mailcow/mailcow-dockerized/issues/6143)...\e[0m"
+      echo -e "\e[91mWARNING!!! \e[31mAn old version of dns_blocklists.cf has been detected which may cause a broken postfix upon startup (see: https://github.com/mailcow/mailcow-dockerized/issues/6143)...\e[0m"
       echo -e "\e[31mIf you have any custom settings in there you might copy it away and adapt the changes after the file is regenerated...\e[0m"
-      read -p "Do you want to delete the file now and let mailcow regenerate it properly? " response
+      read -p "Do you want to delete the file now and let mailcow regenerate it properly? [y/n]" response
       if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
         rm "$file"
         echo -e "\e[32mdns_blocklists.cf has been deleted and will be properly regenerated"
@@ -299,7 +299,7 @@ fix_broken_dnslist_conf() {
         echo -e "\e[35mOk, not deleting it! Please make sure you take a look at postfix upon start then..."
         return 2
       fi
-  fi  
+  fi
 
 }
 
@@ -540,6 +540,7 @@ CONFIG_ARRAY=(
   "SPAMHAUS_DQS_KEY"
   "SKIP_UNBOUND_HEALTHCHECK"
   "DISABLE_NETFILTER_ISOLATION_RULE"
+  "REDISPASS"
 )
 
 detect_bad_asn
@@ -832,6 +833,14 @@ for option in "${CONFIG_ARRAY[@]}"; do
       echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> 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/urandom tr -dc A-Za-z0-9 2> /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