Browse Source

[Dovecot] use python bootstrapper to start DOVECOT container

FreddleSpl0it 3 months ago
parent
commit
2efea9c832
36 changed files with 1223 additions and 690 deletions
  1. 10 2
      data/Dockerfiles/bootstrap/main.py
  2. 122 6
      data/Dockerfiles/bootstrap/modules/BootstrapBase.py
  3. 307 0
      data/Dockerfiles/bootstrap/modules/BootstrapDovecot.py
  4. 30 21
      data/Dockerfiles/dovecot/Dockerfile
  5. 12 329
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  6. 0 2
      data/Dockerfiles/dovecot/maildir_gc.sh
  7. 5 0
      data/Dockerfiles/dovecot/quota_notify.py
  8. 2 2
      data/Dockerfiles/dovecot/supervisord.conf
  9. 2 1
      data/Dockerfiles/nginx/Dockerfile
  10. 3 1
      data/Dockerfiles/postfix/Dockerfile
  11. 2 1
      data/Dockerfiles/sogo/Dockerfile
  12. 1 0
      data/conf/dovecot/config_templates/cron.creds.j2
  13. 14 0
      data/conf/dovecot/config_templates/dovecot-dict-sql-quota.conf.j2
  14. 19 0
      data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_after.conf.j2
  15. 19 0
      data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_before.conf.j2
  16. 4 0
      data/conf/dovecot/config_templates/dovecot-dict-sql-userdb.conf.j2
  17. 3 0
      data/conf/dovecot/config_templates/dovecot-master.passwd.j2
  18. 1 0
      data/conf/dovecot/config_templates/dovecot-master.userdb.j2
  19. 309 0
      data/conf/dovecot/config_templates/dovecot.conf.j2
  20. 308 308
      data/conf/dovecot/config_templates/dovecot.folders.conf.j2
  21. 4 7
      data/conf/dovecot/config_templates/fts.conf.j2
  22. 0 0
      data/conf/dovecot/config_templates/global_sieve_after.sieve.j2
  23. 0 0
      data/conf/dovecot/config_templates/global_sieve_before.sieve.j2
  24. 5 0
      data/conf/dovecot/config_templates/mail_plugins.j2
  25. 5 0
      data/conf/dovecot/config_templates/mail_plugins_imap.j2
  26. 5 0
      data/conf/dovecot/config_templates/mail_plugins_lmtp.j2
  27. 3 0
      data/conf/dovecot/config_templates/mail_replica.conf.j2
  28. 2 0
      data/conf/dovecot/config_templates/maildir_gc.sh.j2
  29. 9 0
      data/conf/dovecot/config_templates/shared_namespace.conf.j2
  30. 1 0
      data/conf/dovecot/config_templates/sieve.creds.j2
  31. 6 0
      data/conf/dovecot/config_templates/sni.conf.j2
  32. 1 0
      data/conf/dovecot/config_templates/sogo-sso.pass.j2
  33. 3 0
      data/conf/dovecot/config_templates/sogo_trusted_ip.conf.j2
  34. 3 0
      data/conf/dovecot/config_templates/source_env.sh.j2
  35. 0 9
      data/conf/dovecot/ldap/passdb.conf
  36. 3 1
      docker-compose.yml

+ 10 - 2
data/Dockerfiles/bootstrap/main.py

@@ -10,13 +10,15 @@ def main():
     from modules.BootstrapNginx import Bootstrap
   elif container_name == "postfix-mailcow":
     from modules.BootstrapPostfix import Bootstrap
+  elif container_name == "dovecot-mailcow":
+    from modules.BootstrapDovecot import Bootstrap
   else:
     print(f"No bootstrap handler for container: {container_name}", file=sys.stderr)
     sys.exit(1)
 
   b = Bootstrap(
     container=container_name,
-    db_config = {
+    db_config={
       "host": "localhost",
       "user": os.getenv("DBUSER"),
       "password": os.getenv("DBPASS"),
@@ -25,7 +27,13 @@ def main():
       'connection_timeout': 2
     },
     db_table="service_settings",
-    db_settings=['sogo']
+    db_settings=['sogo'],
+    redis_config={
+      "host": os.getenv("REDIS_SLAVEOF_IP") or "redis-mailcow",
+      "port": int(os.getenv("REDIS_SLAVEOF_PORT") or 6379),
+      "password": os.getenv("REDISPASS"),
+      "db": 0
+    }
   )
 
   b.bootstrap()

+ 122 - 6
data/Dockerfiles/bootstrap/modules/BootstrapBase.py

@@ -9,21 +9,25 @@ import time
 import socket
 import signal
 import re
+import redis
+import hashlib
 import json
 from pathlib import Path
 import mysql.connector
 from jinja2 import Environment, FileSystemLoader
 
 class BootstrapBase:
-  def __init__(self, container, db_config, db_table, db_settings):
+  def __init__(self, container, db_config, db_table, db_settings, redis_config):
     self.container = container
     self.db_config = db_config
     self.db_table = db_table
     self.db_settings = db_settings
+    self.redis_config = redis_config
 
     self.env = None
     self.env_vars = None
     self.mysql_conn = None
+    self.redis_conn = None
 
   def render_config(self, template_name, output_path):
     """
@@ -184,16 +188,21 @@ class BootstrapBase:
 
     Args:
         path (str or Path): Path to the file or directory.
-        user (str): Username for new owner.
-        group (str, optional): Group name; defaults to user's group if not provided.
+        user (str or int): Username or UID for new owner.
+        group (str or int, optional): Group name or GID; defaults to user's group if not provided.
         recursive (bool): If True and path is a directory, ownership is applied recursively.
 
     Raises:
         FileNotFoundError: If the path does not exist.
     """
 
-    uid = pwd.getpwnam(user).pw_uid
-    gid = grp.getgrnam(group or user).gr_gid
+    # Resolve UID
+    uid = int(user) if str(user).isdigit() else pwd.getpwnam(user).pw_uid
+    # Resolve GID
+    if group is not None:
+      gid = int(group) if str(group).isdigit() else grp.getgrnam(group).gr_gid
+    else:
+      gid = uid if isinstance(user, int) or str(user).isdigit() else grp.getgrnam(user).gr_gid
 
     p = Path(path)
     if not p.exists():
@@ -231,6 +240,67 @@ class BootstrapBase:
 
     shutil.move(str(src_path), str(dst_path))
 
+  def copy_file(self, src, dst, overwrite=True):
+    """
+    Copies a file from src to dst using shutil.
+
+    Args:
+      src (str or Path): Source file path.
+      dst (str or Path): Destination file path.
+      overwrite (bool): Whether to overwrite the destination if it exists.
+
+    Raises:
+      FileNotFoundError: If the source file doesn't exist.
+      FileExistsError: If the destination exists and overwrite is False.
+      IOError: If the copy operation fails.
+    """
+
+    src_path = Path(src)
+    dst_path = Path(dst)
+
+    if not src_path.is_file():
+      raise FileNotFoundError(f"Source file not found: {src_path}")
+
+    if dst_path.exists() and not overwrite:
+      raise FileExistsError(f"Destination exists: {dst_path}")
+
+    dst_path.parent.mkdir(parents=True, exist_ok=True)
+
+    shutil.copy2(src_path, dst_path)
+
+  def remove(self, path, recursive=False, wipe_contents=False):
+    """
+    Removes a file or directory.
+
+    Args:
+      path (str or Path): The file or directory path to remove.
+      recursive (bool): If True, directories will be removed recursively.
+      wipe_contents (bool): If True and path is a directory, only its contents are removed, not the dir itself.
+
+    Raises:
+      FileNotFoundError: If the path does not exist.
+      ValueError: If a directory is passed without recursive or wipe_contents.
+    """
+
+    path = Path(path)
+
+    if not path.exists():
+      raise FileNotFoundError(f"Cannot remove: {path} does not exist")
+
+    if wipe_contents and path.is_dir():
+      for child in path.iterdir():
+        if child.is_dir():
+          shutil.rmtree(child)
+        else:
+          child.unlink()
+    elif path.is_file():
+      path.unlink()
+    elif path.is_dir():
+      if recursive:
+        shutil.rmtree(path)
+      else:
+        raise ValueError(f"{path} is a directory. Use recursive=True or wipe_contents=True to remove it.")
+
   def create_dir(self, path):
     """
     Creates a directory if it does not exist.
@@ -376,6 +446,49 @@ class BootstrapBase:
     if self.mysql_conn and self.mysql_conn.is_connected():
       self.mysql_conn.close()
 
+  def connect_redis(self, retries=10, delay=2):
+    """
+    Establishes a Redis connection and stores it in `self.redis_conn`.
+
+    Args:
+      retries (int): Number of ping retries before giving up.
+      delay (int): Seconds between retries.
+    """
+
+    client = redis.Redis(
+      host=self.redis_config['host'],
+      port=self.redis_config['port'],
+      password=self.redis_config['password'],
+      db=self.redis_config['db'],
+      decode_responses=True
+    )
+
+    for _ in range(retries):
+      try:
+        if client.ping():
+          self.redis_conn = client
+          return
+      except redis.RedisError as e:
+        print(f"Waiting for Redis... ({e})")
+        time.sleep(delay)
+
+    raise ConnectionError("Redis is not available after multiple attempts.")
+
+  def close_redis(self):
+    """
+    Closes the Redis connection if it's open.
+
+    Safe to call even if Redis was never connected or already closed.
+    """
+
+    if self.redis_conn:
+      try:
+        self.redis_conn.close()
+      except Exception as e:
+        print(f"Error while closing Redis connection: {e}")
+      finally:
+        self.redis_conn = None
+
   def wait_for_schema_update(self, init_file_path="init_db.inc.php", check_interval=5):
     """
     Waits until the current database schema version matches the expected version
@@ -559,4 +672,7 @@ class BootstrapBase:
       print(e.stderr.strip())
       if check:
         raise
-      return e
+      return e
+
+  def sha1_filter(self, value):
+    return hashlib.sha1(value.encode()).hexdigest()

+ 307 - 0
data/Dockerfiles/bootstrap/modules/BootstrapDovecot.py

@@ -0,0 +1,307 @@
+from jinja2 import Environment, FileSystemLoader
+from modules.BootstrapBase import BootstrapBase
+from pathlib import Path
+import os
+import sys
+import time
+import pwd
+import hashlib
+
+class Bootstrap(BootstrapBase):
+  def bootstrap(self):
+    # Connect to MySQL
+    self.connect_mysql()
+    self.wait_for_schema_update()
+
+    # Connect to Redis
+    self.connect_redis()
+    self.redis_conn.set("DOVECOT_REPL_HEALTH", 1)
+
+    # Wait for DNS
+    self.wait_for_dns("mailcow.email")
+
+    # Create missing directories
+    self.create_dir("/etc/dovecot/sql/")
+    self.create_dir("/etc/dovecot/auth/")
+    self.create_dir("/var/vmail/_garbage")
+    self.create_dir("/var/vmail/sieve")
+    self.create_dir("/etc/sogo")
+    self.create_dir("/var/volatile")
+
+    # Setup Jinja2 Environment and load vars
+    self.env = Environment(
+      loader=FileSystemLoader('./etc/dovecot/config_templates'),
+      keep_trailing_newline=True,
+      lstrip_blocks=True,
+      trim_blocks=True
+    )
+    extra_vars = {
+      "VALID_CERT_DIRS": self.get_valid_cert_dirs(),
+      "RAND_USER": self.rand_pass(),
+      "RAND_PASS": self.rand_pass(),
+      "RAND_PASS2": self.rand_pass(),
+      "ENV_VARS": dict(os.environ)
+    }
+    self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars)
+    # Escape DBPASS
+    self.env_vars['DBPASS'] = self.env_vars['DBPASS'].replace('"', r'\"')
+    # Set custom filters
+    self.env.filters['sha1'] = self.sha1_filter
+
+    print("Set Timezone")
+    self.set_timezone()
+
+    print("Render config")
+    self.render_config("dovecot-dict-sql-quota.conf.j2", "/etc/dovecot/sql/dovecot-dict-sql-quota.conf")
+    self.render_config("dovecot-dict-sql-userdb.conf.j2", "/etc/dovecot/sql/dovecot-dict-sql-userdb.conf")
+    self.render_config("dovecot-dict-sql-sieve_before.conf.j2", "/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf")
+    self.render_config("dovecot-dict-sql-sieve_after.conf.j2", "/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf")
+    self.render_config("mail_plugins.j2", "/etc/dovecot/mail_plugins")
+    self.render_config("mail_plugins_imap.j2", "/etc/dovecot/mail_plugins_imap")
+    self.render_config("mail_plugins_lmtp.j2", "/etc/dovecot/mail_plugins_lmtp")
+    self.render_config("global_sieve_after.sieve.j2", "/var/vmail/sieve/global_sieve_after.sieve")
+    self.render_config("global_sieve_before.sieve.j2", "/var/vmail/sieve/global_sieve_before.sieve")
+    self.render_config("dovecot-master.passwd.j2", "/etc/dovecot/dovecot-master.passwd")
+    self.render_config("dovecot-master.userdb.j2", "/etc/dovecot/dovecot-master.userdb")
+    self.render_config("sieve.creds.j2", "/etc/sogo/sieve.creds")
+    self.render_config("sogo-sso.pass.j2", "/etc/phpfpm/sogo-sso.pass")
+    self.render_config("cron.creds.j2", "/etc/sogo/cron.creds")
+    self.render_config("source_env.sh.j2", "/source_env.sh")
+    self.render_config("maildir_gc.sh.j2", "/usr/local/bin/maildir_gc.sh")
+    self.render_config("dovecot.conf.j2", "/etc/dovecot/dovecot.conf")
+
+    files = [
+      "/etc/dovecot/mail_plugins",
+      "/etc/dovecot/mail_plugins_imap",
+      "/etc/dovecot/mail_plugins_lmtp",
+      "/templates/quarantine.tpl"
+    ]
+    for file in files:
+      self.set_permissions(file, 0o644)
+
+    try:
+      # Migrate old sieve_after file
+      self.move_file("/etc/dovecot/sieve_after", "/var/vmail/sieve/global_sieve_after.sieve")
+    except Exception as e:
+      pass
+    try:
+      # Cleanup random user maildirs
+      self.remove("/var/vmail/mailcow.local", wipe_contents=True)
+    except Exception as e:
+      pass
+    try:
+      # Cleanup PIDs
+      self.remove("/tmp/quarantine_notify.pid")
+    except Exception as e:
+      pass
+    try:
+      self.remove("/var/run/dovecot/master.pid")
+    except Exception as e:
+      pass
+
+    # Check permissions of vmail/index/garbage directories.
+    # Do not do this every start-up, it may take a very long time. So we use a stat check here.
+    files = [
+      "/var/vmail",
+      "/var/vmail/_garbage",
+      "/var/vmail_index"
+    ]
+    for file in files:
+      path = Path(file)
+      try:
+        stat_info = path.stat()
+        current_user = pwd.getpwuid(stat_info.st_uid).pw_name
+
+        if current_user != "vmail":
+          print(f"Ownership of {path} is {current_user}, fixing to vmail:vmail...")
+          self.set_owner(path, user="vmail", group="vmail", recursive=True)
+        else:
+          print(f"Ownership of {path} is already correct (vmail)")
+      except Exception as e:
+          print(f"Error checking ownership of {path}: {e}")
+
+    # Compile sieve scripts
+    files = [
+      "/var/vmail/sieve/global_sieve_before.sieve",
+      "/var/vmail/sieve/global_sieve_after.sieve",
+      "/usr/lib/dovecot/sieve/report-spam.sieve",
+      "/usr/lib/dovecot/sieve/report-ham.sieve",
+    ]
+    for file in files:
+      self.run_command(["sievec", file], check=False)
+
+    # Fix permissions
+    for path in Path("/etc/dovecot/sql").glob("*.conf"):
+      self.set_owner(path, "root", "root")
+      self.set_permissions(path, 0o640)
+
+    files = [
+      "/etc/dovecot/auth/passwd-verify.lua",
+      *Path("/etc/dovecot/sql").glob("dovecot-dict-sql-sieve*"),
+      *Path("/etc/dovecot/sql").glob("dovecot-dict-sql-quota*")
+    ]
+    for file in files:
+      self.set_owner(file, "root", "dovecot")
+
+    self.set_permissions("/etc/dovecot/auth/passwd-verify.lua", 0o640)
+
+    for file in ["/var/vmail/sieve", "/var/volatile", "/var/vmail_index"]:
+      self.set_owner(file, "vmail", "vmail", recursive=True)
+
+    self.run_command(["adduser", "vmail", "tty"])
+    self.run_command(["chmod", "g+rw", "/dev/console"])
+    self.set_owner("/dev/console", "root", "tty")
+    files = [
+      "/usr/lib/dovecot/sieve/rspamd-pipe-ham",
+      "/usr/lib/dovecot/sieve/rspamd-pipe-spam",
+      "/usr/local/bin/imapsync_runner.pl",
+      "/usr/local/bin/imapsync",
+      "/usr/local/bin/trim_logs.sh",
+      "/usr/local/bin/sa-rules.sh",
+      "/usr/local/bin/clean_q_aged.sh",
+      "/usr/local/bin/maildir_gc.sh",
+      "/usr/local/sbin/stop-supervisor.sh",
+      "/usr/local/bin/quota_notify.py",
+      "/usr/local/bin/repl_health.sh",
+      "/usr/local/bin/optimize-fts.sh"
+    ]
+    for file in files:
+      self.set_permissions(file, 0o755)
+
+    # Collect SA rules once now
+    self.run_command(["/usr/local/bin/sa-rules.sh"], check=False)
+
+    self.generate_mail_crypt_keys()
+    self.cleanup_imapsync_jobs()
+    self.generate_guid_version()
+
+  def get_valid_cert_dirs(self):
+    """
+    Returns a mapping of domains to their certificate directory path.
+
+    Example:
+        {
+            "example.com": "/etc/ssl/mail/example.com/",
+            "www.example.com": "/etc/ssl/mail/example.com/"
+        }
+    """
+    sni_map = {}
+    base_path = Path("/etc/ssl/mail")
+    if not base_path.exists():
+      return sni_map
+
+    for cert_dir in base_path.iterdir():
+      if not cert_dir.is_dir():
+        continue
+
+      domains_file = cert_dir / "domains"
+      cert_file = cert_dir / "cert.pem"
+      key_file = cert_dir / "key.pem"
+
+      if not (domains_file.exists() and cert_file.exists() and key_file.exists()):
+        continue
+
+      with open(domains_file, "r") as f:
+        domains = [line.strip() for line in f if line.strip()]
+        for domain in domains:
+          sni_map[domain] = str(cert_dir)
+
+    return sni_map
+
+  def generate_mail_crypt_keys(self):
+    """
+    Ensures mail_crypt EC keypair exists. Generates if missing. Adjusts permissions.
+    """
+
+    key_dir = Path("/mail_crypt")
+    priv_key = key_dir / "ecprivkey.pem"
+    pub_key = key_dir / "ecpubkey.pem"
+
+    # Generate keys if they don't exist or are empty
+    if not priv_key.exists() or priv_key.stat().st_size == 0 or \
+      not pub_key.exists() or pub_key.stat().st_size == 0:
+      self.run_command(
+        "openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem",
+        shell=True
+      )
+      self.run_command(
+        "openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem",
+        shell=True
+      )
+
+    # Set ownership to UID 401 (dovecot)
+    self.set_owner(priv_key, user='401')
+    self.set_owner(pub_key, user='401')
+
+  def cleanup_imapsync_jobs(self):
+    """
+    Cleans up stale imapsync locks and resets running status in the database.
+
+    Deletes the imapsync_busy.lock file if present and sets `is_running` to 0
+    in the `imapsync` table, if it exists.
+
+    Logs:
+      Any issues with file operations or SQL execution.
+    """
+
+    lock_file = Path("/tmp/imapsync_busy.lock")
+    if lock_file.exists():
+      try:
+        lock_file.unlink()
+      except Exception as e:
+        print(f"Failed to remove lock file: {e}")
+
+    try:
+      cursor = self.mysql_conn.cursor()
+      cursor.execute("SHOW TABLES LIKE 'imapsync'")
+      result = cursor.fetchone()
+      if result:
+        cursor.execute("UPDATE imapsync SET is_running='0'")
+        self.mysql_conn.commit()
+      cursor.close()
+    except Exception as e:
+      print(f"Error updating imapsync table: {e}")
+
+  def generate_guid_version(self):
+    """
+    Waits for the `versions` table to be created, then generates a GUID
+    based on the mail hostname and Dovecot's public key and inserts it
+    into the `versions` table.
+
+    If the key or hash is missing or malformed, marks it as INVALID.
+    """
+
+    try:
+      result = self.run_command(["doveconf", "-P"], check=True)
+      pubkey_path = None
+      for line in result.stdout.splitlines():
+        if "mail_crypt_global_public_key" in line:
+          parts = line.split('<')
+          if len(parts) > 1:
+            pubkey_path = parts[1].strip()
+            break
+
+      if pubkey_path and Path(pubkey_path).exists():
+        with open(pubkey_path, "rb") as key_file:
+          pubkey_data = key_file.read()
+
+        hostname = self.env_vars.get("MAILCOW_HOSTNAME", "mailcow.local").encode("utf-8")
+        concat = hostname + pubkey_data
+        guid = hashlib.sha256(concat).hexdigest()
+
+        if len(guid) == 64:
+          version_value = guid
+        else:
+          version_value = "INVALID"
+
+        cursor = self.mysql_conn.cursor()
+        cursor.execute(
+          "REPLACE INTO versions (application, version) VALUES (%s, %s)",
+          ("GUID", version_value)
+        )
+        self.mysql_conn.commit()
+        cursor.close()
+      else:
+        print("Public key not found or unreadable. GUID not generated.")
+    except Exception as e:
+      print(f"Failed to generate or store GUID: {e}")

+ 30 - 21
data/Dockerfiles/dovecot/Dockerfile

@@ -87,7 +87,7 @@ RUN addgroup -g 5000 vmail \
   perl-proc-processtable \
   perl-app-cpanminus \
   procps \
-  python3 \
+  python3 py3-pip \
   py3-mysqlclient \
   py3-html2text \
   py3-jinja2 \
@@ -115,25 +115,34 @@ RUN addgroup -g 5000 vmail \
   && chmod +x /usr/local/bin/gosu \
   && gosu nobody true
 
-COPY trim_logs.sh /usr/local/bin/trim_logs.sh
-COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
-COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
-COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
-COPY imapsync /usr/local/bin/imapsync
-COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
-COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
-COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
-COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
-COPY rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam
-COPY sa-rules.sh /usr/local/bin/sa-rules.sh
-COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh
-COPY docker-entrypoint.sh /
-COPY supervisord.conf /etc/supervisor/supervisord.conf
-COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
-COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py
-COPY quota_notify.py /usr/local/bin/quota_notify.py
-COPY repl_health.sh /usr/local/bin/repl_health.sh
-COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh
+RUN pip install  --break-system-packages \
+  mysql-connector-python \
+  jinja2 \
+  redis
+
+
+COPY data/Dockerfiles/bootstrap /bootstrap
+COPY data/Dockerfiles/dovecot/trim_logs.sh /usr/local/bin/trim_logs.sh
+COPY data/Dockerfiles/dovecot/clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
+COPY data/Dockerfiles/dovecot/syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
+COPY data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
+COPY data/Dockerfiles/dovecot/imapsync /usr/local/bin/imapsync
+COPY data/Dockerfiles/dovecot/imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
+COPY data/Dockerfiles/dovecot/report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
+COPY data/Dockerfiles/dovecot/report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
+COPY data/Dockerfiles/dovecot/rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
+COPY data/Dockerfiles/dovecot/rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam
+COPY data/Dockerfiles/dovecot/sa-rules.sh /usr/local/bin/sa-rules.sh
+COPY data/Dockerfiles/dovecot/docker-entrypoint.sh /
+COPY data/Dockerfiles/dovecot/supervisord.conf /etc/supervisor/supervisord.conf
+COPY data/Dockerfiles/dovecot/stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
+COPY data/Dockerfiles/dovecot/quarantine_notify.py /usr/local/bin/quarantine_notify.py
+COPY data/Dockerfiles/dovecot/quota_notify.py /usr/local/bin/quota_notify.py
+COPY data/Dockerfiles/dovecot/repl_health.sh /usr/local/bin/repl_health.sh
+COPY data/Dockerfiles/dovecot/optimize-fts.sh /usr/local/bin/optimize-fts.sh
+
+RUN chmod +x /docker-entrypoint.sh \
+  /usr/local/sbin/stop-supervisor.sh
+
 
-ENTRYPOINT ["/docker-entrypoint.sh"]
 CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

+ 12 - 329
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -1,253 +1,15 @@
 #!/bin/bash
-set -e
 
-# Wait for MySQL to warm-up
-while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
-  echo "Waiting for database to come up..."
-  sleep 2
-done
-
-until dig +short mailcow.email > /dev/null; do
-  echo "Waiting for DNS..."
-  sleep 1
-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} -a ${REDISPASS} --no-auth-warning"
-else
-  REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
-fi
-
-until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
-  echo "Waiting for Redis..."
-  sleep 2
-done
-
-${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
-
-# Create missing directories
-[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
-[[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/
-[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
-[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
-[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
-[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
-[[ ! -d /var/volatile ]] && mkdir -p /var/volatile
-
-# Set Dovecot sql config parameters, escape " in db password
-DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
-
-# Create quota dict for Dovecot
-if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-  QUOTA_TABLE=quota2
-else
-  QUOTA_TABLE=quota2replica
-fi
-cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
-# Autogenerated by mailcow
-connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
-map {
-  pattern = priv/quota/storage
-  table = ${QUOTA_TABLE}
-  username_field = username
-  value_field = bytes
-}
-map {
-  pattern = priv/quota/messages
-  table = ${QUOTA_TABLE}
-  username_field = username
-  value_field = messages
-}
-EOF
-
-# Create dict used for sieve pre and postfilters
-cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
-# Autogenerated by mailcow
-connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
-map {
-  pattern = priv/sieve/name/\$script_name
-  table = sieve_before
-  username_field = username
-  value_field = id
-  fields {
-    script_name = \$script_name
-  }
-}
-map {
-  pattern = priv/sieve/data/\$id
-  table = sieve_before
-  username_field = username
-  value_field = script_data
-  fields {
-    id = \$id
-  }
-}
-EOF
-
-cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
-# Autogenerated by mailcow
-connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
-map {
-  pattern = priv/sieve/name/\$script_name
-  table = sieve_after
-  username_field = username
-  value_field = id
-  fields {
-    script_name = \$script_name
-  }
-}
-map {
-  pattern = priv/sieve/data/\$id
-  table = sieve_after
-  username_field = username
-  value_field = script_data
-  fields {
-    id = \$id
-  }
-}
-EOF
-
-echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
-
-if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
-echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
-echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
-echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
-else
-echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
-echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
-echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
-echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
-fi
-chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
-
-cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
-# Autogenerated by mailcow
-driver = mysql
-connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
-user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
-iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
-EOF
-
-
-# Migrate old sieve_after file
-[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
-# Create global sieve scripts
-cat /etc/dovecot/global_sieve_after > /var/vmail/sieve/global_sieve_after.sieve
-cat /etc/dovecot/global_sieve_before > /var/vmail/sieve/global_sieve_before.sieve
-
-# Check permissions of vmail/index/garbage directories.
-# Do not do this every start-up, it may take a very long time. So we use a stat check here.
-if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi
-if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi
-if [[ $(stat -c %U /var/vmail_index) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail_index ; fi
-
-# Cleanup random user maildirs
-rm -rf /var/vmail/mailcow.local/*
-# Cleanup PIDs
-[[ -f /tmp/quarantine_notify.pid ]] && rm /tmp/quarantine_notify.pid
-
-# create sni configuration
-echo "" > /etc/dovecot/sni.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
+# Run hooks
+for file in /hooks/*; do
+  if [ -x "${file}" ]; then
+    echo "Running hook ${file}"
+    "${file}"
   fi
-  domains=($(cat ${cert_dir}domains))
-  for domain in ${domains[@]}; do
-    echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
-    echo '  ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
-    echo '  ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
-    echo '}' >> /etc/dovecot/sni.conf;
-  done
 done
 
-# Create random master for SOGo sieve features
-RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
-RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
-
-if [[ ! -z ${DOVECOT_MASTER_USER} ]] && [[ ! -z ${DOVECOT_MASTER_PASS} ]]; then
-  RAND_USER=${DOVECOT_MASTER_USER}
-  RAND_PASS=${DOVECOT_MASTER_PASS}
-fi
-echo ${RAND_USER}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}'):::::: > /etc/dovecot/dovecot-master.passwd
-echo ${RAND_USER}@mailcow.local::5000:5000:::: > /etc/dovecot/dovecot-master.userdb
-echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds
-
-if [[ -z ${MAILDIR_SUB} ]]; then
-  MAILDIR_SUB_SHARED=
-else
-  MAILDIR_SUB_SHARED=/${MAILDIR_SUB}
-fi
-cat <<EOF > /etc/dovecot/shared_namespace.conf
-# Autogenerated by mailcow
-namespace {
-    type = shared
-    separator = /
-    prefix = Shared/%%u/
-    location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u
-    subscriptions = no
-    list = children
-}
-EOF
-
-
-cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
-# Autogenerated by mailcow
-remote ${IPV4_NETWORK}.248 {
-  disable_plaintext_auth = no
-}
-EOF
-
-# Create random master Password for SOGo SSO
-RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
-echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
-# Creating additional creds file for SOGo notify crons (calendars, etc)
-echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
-cat <<EOF > /etc/dovecot/sogo-sso.conf
-# Autogenerated by mailcow
-passdb {
-  driver = static
-  args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
-}
-EOF
-
-if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
-  # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
-  cat <<'EOF' > /usr/local/bin/quota_notify.py
-#!/usr/bin/python3
-import sys
-sys.exit()
-EOF
-fi
-
-# Set mail_replica for HA setups
-if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then
-  cat <<EOF > /etc/dovecot/mail_replica.conf
-# Autogenerated by mailcow
-mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT}
-EOF
-fi
-
-# Setting variables for indexer-worker inside fts.conf automatically according to mailcow.conf settings
-if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
-  echo -e "\e[94mConfiguring FTS Settings...\e[0m"
-  echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\e[0m"
-  sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/fts.conf
-  echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m"
-  sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/fts.conf
-fi
-
-# 401 is user dovecot
-if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
-	openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
-	openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem
-	chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
-else
-	chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
-fi
+python3 -u /bootstrap/main.py
+BOOTSTRAP_EXIT_CODE=$?
 
 # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
 if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
@@ -260,89 +22,10 @@ if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.c
     echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
 fi
 
-# Compile sieve scripts
-sievec /var/vmail/sieve/global_sieve_before.sieve
-sievec /var/vmail/sieve/global_sieve_after.sieve
-sievec /usr/lib/dovecot/sieve/report-spam.sieve
-sievec /usr/lib/dovecot/sieve/report-ham.sieve
-
-# Fix permissions
-chown root:root /etc/dovecot/sql/*.conf
-chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua
-chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua
-chown -R vmail:vmail /var/vmail/sieve
-chown -R vmail:vmail /var/volatile
-chown -R vmail:vmail /var/vmail_index
-adduser vmail tty
-chmod g+rw /dev/console
-chown root:tty /dev/console
-chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
-  /usr/lib/dovecot/sieve/rspamd-pipe-spam \
-  /usr/local/bin/imapsync_runner.pl \
-  /usr/local/bin/imapsync \
-  /usr/local/bin/trim_logs.sh \
-  /usr/local/bin/sa-rules.sh \
-  /usr/local/bin/clean_q_aged.sh \
-  /usr/local/bin/maildir_gc.sh \
-  /usr/local/sbin/stop-supervisor.sh \
-  /usr/local/bin/quota_notify.py \
-  /usr/local/bin/repl_health.sh \
-  /usr/local/bin/optimize-fts.sh
-
-# Prepare environment file for cronjobs
-printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
-
-# Clean old PID if any
-[[ -f /var/run/dovecot/master.pid ]] && rm /var/run/dovecot/master.pid
-
-# Clean stopped imapsync jobs
-rm -f /tmp/imapsync_busy.lock
-IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
-[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
-
-# Envsubst maildir_gc
-echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
-
-# GUID generation
-while [[ ${VERSIONS_OK} != 'OK' ]]; do
-  if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
-    VERSIONS_OK=OK
-  else
-    echo "Waiting for versions table to be created..."
-    sleep 3
-  fi
-done
-PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key | cut -d '<' -f2)
-if [ -f ${PUBKEY_MCRYPT} ]; then
-  GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
-  if [ ${#GUID} -eq 64 ]; then
-    mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
-EOF
-  else
-    mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
-EOF
-  fi
-fi
-
-# Collect SA rules once now
-/usr/local/bin/sa-rules.sh
-
-# Run hooks
-for file in /hooks/*; do
-  if [ -x "${file}" ]; then
-    echo "Running hook ${file}"
-    "${file}"
-  fi
-done
-
-# For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
-# May be related to something inside Docker, I seriously don't know
-touch /etc/dovecot/auth/passwd-verify.lua
-
-if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
-  cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
+if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
+  echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Dovecot."
+  exit $BOOTSTRAP_EXIT_CODE
 fi
 
-exec "$@"
+echo "Bootstrap succeeded. Starting Dovecot..."
+/usr/sbin/dovecot -F

+ 0 - 2
data/Dockerfiles/dovecot/maildir_gc.sh

@@ -1,2 +0,0 @@
-#!/bin/bash
-[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r {} \;

+ 5 - 0
data/Dockerfiles/dovecot/quota_notify.py

@@ -14,6 +14,11 @@ import sys
 import html2text
 from subprocess import Popen, PIPE, STDOUT
 
+
+# Don't run if role is not master
+if os.getenv("MASTER").lower() in ["n", "no"]:
+  sys.exit()
+
 if len(sys.argv) > 2:
   percent = int(sys.argv[1])
   username = str(sys.argv[2])

+ 2 - 2
data/Dockerfiles/dovecot/supervisord.conf

@@ -11,8 +11,8 @@ stderr_logfile=/dev/stderr
 stderr_logfile_maxbytes=0
 autostart=true
 
-[program:dovecot]
-command=/usr/sbin/dovecot -F
+[program:bootstrap]
+command=/docker-entrypoint.sh
 stdout_logfile=/dev/stdout
 stdout_logfile_maxbytes=0
 stderr_logfile=/dev/stderr

+ 2 - 1
data/Dockerfiles/nginx/Dockerfile

@@ -9,7 +9,8 @@ RUN apk add --no-cache nginx \
   py3-pip && \
   pip install --upgrade pip && \
   pip install Jinja2 \
-  mysql-connector-python
+  mysql-connector-python \
+  redis
 
 RUN mkdir -p /etc/nginx/includes
 

+ 3 - 1
data/Dockerfiles/postfix/Dockerfile

@@ -42,7 +42,8 @@ RUN groupadd -g 102 postfix \
 
 RUN pip install  --break-system-packages \
   mysql-connector-python \
-  jinja2
+  jinja2 \
+	redis
 
 COPY data/Dockerfiles/bootstrap /bootstrap
 COPY data/Dockerfiles/postfix/supervisord.conf /etc/supervisor/supervisord.conf
@@ -55,6 +56,7 @@ COPY data/Dockerfiles/postfix/stop-supervisor.sh /usr/local/sbin/stop-supervisor
 COPY data/Dockerfiles/postfix/docker-entrypoint.sh /docker-entrypoint.sh
 
 RUN chmod +x /usr/local/bin/rspamd-pipe-ham \
+	/docker-entrypoint.sh \
   /usr/local/bin/rspamd-pipe-spam \
   /usr/local/bin/whitelist_forwardinghosts.sh \
   /usr/local/sbin/stop-supervisor.sh

+ 2 - 1
data/Dockerfiles/sogo/Dockerfile

@@ -45,7 +45,8 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
 
 RUN pip install  --break-system-packages \
   mysql-connector-python \
-  jinja2
+  jinja2 \
+  redis
 
 
 COPY data/Dockerfiles/bootstrap /bootstrap

+ 1 - 0
data/conf/dovecot/config_templates/cron.creds.j2

@@ -0,0 +1 @@
+{{ RAND_USER }}@mailcow.local:{{ RAND_PASS2 }}

+ 14 - 0
data/conf/dovecot/config_templates/dovecot-dict-sql-quota.conf.j2

@@ -0,0 +1,14 @@
+{% set QUOTA_TABLE = "quota2" if MASTER|lower in ["y", "yes"] else "quota2replica" %}
+connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS }}"
+map {
+  pattern = priv/quota/storage
+  table = {{ QUOTA_TABLE }}
+  username_field = username
+  value_field = bytes
+}
+map {
+  pattern = priv/quota/messages
+  table = {{ QUOTA_TABLE }}
+  username_field = username
+  value_field = messages
+}

+ 19 - 0
data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_after.conf.j2

@@ -0,0 +1,19 @@
+connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS }}"
+map {
+  pattern = priv/sieve/name/\$script_name
+  table = sieve_after
+  username_field = username
+  value_field = id
+  fields {
+    script_name = \$script_name
+  }
+}
+map {
+  pattern = priv/sieve/data/\$id
+  table = sieve_after
+  username_field = username
+  value_field = script_data
+  fields {
+    id = \$id
+  }
+}

+ 19 - 0
data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_before.conf.j2

@@ -0,0 +1,19 @@
+connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS }}"
+map {
+  pattern = priv/sieve/name/\$script_name
+  table = sieve_before
+  username_field = username
+  value_field = id
+  fields {
+    script_name = \$script_name
+  }
+}
+map {
+  pattern = priv/sieve/data/\$id
+  table = sieve_before
+  username_field = username
+  value_field = script_data
+  fields {
+    id = \$id
+  }
+}

+ 4 - 0
data/conf/dovecot/config_templates/dovecot-dict-sql-userdb.conf.j2

@@ -0,0 +1,4 @@
+driver = mysql
+connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS }}"
+user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/{{ MAILDIR_SUB }}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
+iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';

+ 3 - 0
data/conf/dovecot/config_templates/dovecot-master.passwd.j2

@@ -0,0 +1,3 @@
+{%- set master_user = DOVECOT_MASTER_USER or RAND_USER %}
+{%- set master_pass = DOVECOT_MASTER_PASS or RAND_PASS %}
+{{ master_user }}@mailcow.local:{SHA1}{{ master_pass | sha1 }}::::::

+ 1 - 0
data/conf/dovecot/config_templates/dovecot-master.userdb.j2

@@ -0,0 +1 @@
+{{ DOVECOT_MASTER_USER or RAND_USER }}@mailcow.local::5000:5000::::

+ 309 - 0
data/conf/dovecot/config_templates/dovecot.conf.j2

@@ -0,0 +1,309 @@
+auth_mechanisms = plain login
+#mail_debug = yes
+#auth_debug = yes
+#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
+log_path = syslog
+disable_plaintext_auth = yes
+
+# Uncomment on NFS share
+#mmap_disable = yes
+#mail_fsync = always
+#mail_nfs_index = yes
+#mail_nfs_storage = yes
+
+login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
+mail_home = /var/vmail/%d/%n
+mail_location = maildir:~/
+mail_plugins = </etc/dovecot/mail_plugins
+mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
+mail_attachment_dir = /var/attachments
+mail_attachment_min_size = 128k
+# Significantly speeds up very large mailboxes, but is only safe to enable if
+# you do not manually modify the files in the `cur` directories in
+# mailcowdockerized_vmail-vol-1.
+# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
+maildir_very_dirty_syncs = yes
+
+# Dovecot 2.2
+#ssl_protocols = !SSLv3
+# Dovecot 2.3
+ssl_min_protocol = TLSv1.2
+
+ssl_prefer_server_ciphers = yes
+ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
+
+# Default in Dovecot 2.3
+ssl_options = no_compression no_ticket
+
+# New in Dovecot 2.3
+ssl_dh = </etc/ssl/mail/dhparams.pem
+# Dovecot 2.2
+#ssl_dh_parameters_length = 2048
+log_timestamp = "%Y-%m-%d %H:%M:%S "
+recipient_delimiter = +
+auth_master_user_separator = *
+mail_shared_explicit_inbox = yes
+mail_prefetch_count = 30
+passdb {
+  driver = lua
+  args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
+  result_success = return-ok
+  result_failure = continue
+  result_internalfail = continue
+}
+# try a master passwd
+passdb {
+  driver = passwd-file
+  args = /etc/dovecot/dovecot-master.passwd
+  master = yes
+  skip = authenticated
+}
+# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
+# a return of the following passdb is mandatory
+passdb {
+  driver = lua
+  args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
+}
+# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
+service doveadm {
+  inet_listener {
+    port = 12345
+  }
+  vsz_limit=2048 MB
+}
+
+{% include 'dovecot.folders.conf.j2' %}
+
+protocols = imap sieve lmtp pop3
+service dict {
+  unix_listener dict {
+    mode = 0660
+    user = vmail
+    group = vmail
+  }
+}
+service log {
+  user = dovenull
+}
+service config {
+  unix_listener config {
+    user = root
+    group = vmail
+    mode = 0660
+  }
+}
+service auth {
+  inet_listener auth-inet {
+    port = 10001
+  }
+  unix_listener auth-master {
+    mode = 0600
+    user = vmail
+  }
+  unix_listener auth-userdb {
+    mode = 0600
+    user = vmail
+  }
+  vsz_limit = 2G
+}
+service managesieve-login {
+  inet_listener sieve {
+    port = 4190
+  }
+  inet_listener sieve_haproxy {
+    port = 14190
+    haproxy = yes
+  }
+  service_count = 1
+  process_min_avail = 2
+  vsz_limit = 1G
+}
+service imap-login {
+  service_count = 1
+  process_min_avail = 2
+  process_limit = 10000
+  vsz_limit = 1G
+  user = dovenull
+  inet_listener imap_haproxy {
+    port = 10143
+    haproxy = yes
+  }
+  inet_listener imaps_haproxy {
+    port = 10993
+    ssl = yes
+    haproxy = yes
+  }
+}
+service pop3-login {
+  service_count = 1
+  process_min_avail = 1
+  vsz_limit = 1G
+  inet_listener pop3_haproxy {
+    port = 10110
+    haproxy = yes
+  }
+  inet_listener pop3s_haproxy {
+    port = 10995
+    ssl = yes
+    haproxy = yes
+  }
+}
+service imap {
+  executable = imap
+  user = vmail
+  vsz_limit = 1G
+}
+service managesieve {
+  process_limit = 256
+}
+service lmtp {
+  inet_listener lmtp-inet {
+    port = 24
+  }
+  user = vmail
+}
+listen = *,[::]
+ssl_cert = </etc/ssl/mail/cert.pem
+ssl_key = </etc/ssl/mail/key.pem
+userdb {
+  driver = passwd-file
+  args = /etc/dovecot/dovecot-master.userdb
+}
+userdb {
+  args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
+  driver = sql
+  skip = found
+}
+protocol imap {
+  mail_plugins = </etc/dovecot/mail_plugins_imap
+  imap_metadata = yes
+}
+mail_attribute_dict = file:%h/dovecot-attributes
+protocol lmtp {
+  mail_plugins = </etc/dovecot/mail_plugins_lmtp
+  auth_socket_path = /var/run/dovecot/auth-master
+}
+protocol sieve {
+  managesieve_logout_format = bytes=%i/%o
+}
+plugin {
+  # Allow "any" or "authenticated" to be used in ACLs
+  acl_anyone = {{ ACL_ANYONE }}
+  acl_shared_dict = file:/var/vmail/shared-mailboxes.db
+  acl = vfile
+  acl_user = %u
+  quota = dict:Userquota::proxy::sqlquota
+  quota_rule2 = Trash:storage=+100%%
+  sieve = /var/vmail/sieve/%u.sieve
+  sieve_plugins = sieve_imapsieve sieve_extprograms
+  sieve_vacation_send_from_recipient = yes
+  sieve_redirect_envelope_from = recipient
+  # From elsewhere to Spam folder
+  imapsieve_mailbox1_name = Junk
+  imapsieve_mailbox1_causes = COPY
+  imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
+  # END
+  # From Spam folder to elsewhere
+  imapsieve_mailbox2_name = *
+  imapsieve_mailbox2_from = Junk
+  imapsieve_mailbox2_causes = COPY
+  imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
+  # END
+  master_user = %u
+  quota_warning = storage=95%% quota-warning 95 %u
+  quota_warning2 = storage=80%% quota-warning 80 %u
+  sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
+  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
+  sieve_extensions = +notify +imapflags +vacation-seconds +editheader
+  sieve_max_script_size = 1M
+  sieve_max_redirects = 100
+  sieve_max_actions = 101
+  sieve_quota_max_scripts = 0
+  sieve_quota_max_storage = 0
+  listescape_char = "\\"
+  sieve_vacation_min_period = 5s
+  sieve_vacation_max_period = 0
+  sieve_vacation_default_period = 60s
+  sieve_before = /var/vmail/sieve/global_sieve_before.sieve
+  sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
+  sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
+  sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve
+  sieve_duplicate_default_period = 1m
+  sieve_duplicate_max_period = 7d
+
+  # -- Global keys
+  mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
+  mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
+  mail_crypt_save_version = 2
+
+  # Enable compression while saving, lz4 Dovecot v2.3.17+
+  zlib_save = lz4
+
+  mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
+  mail_log_fields = uid box msgid size
+  mail_log_cached_only = yes
+
+  # Try set mail_replica
+  {% include 'mail_replica.conf.j2' %}
+}
+service quota-warning {
+  executable = script /usr/local/bin/quota_notify.py
+  # use some unprivileged user for executing the quota warnings
+  user = vmail
+  unix_listener quota-warning {
+    user = vmail
+  }
+}
+dict {
+  sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf
+  sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
+  sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
+}
+remote 127.0.0.1 {
+  disable_plaintext_auth = no
+}
+submission_host = postfix:588
+mail_max_userip_connections = 500
+service stats {
+  unix_listener stats-writer {
+    mode = 0660
+    user = vmail
+  }
+}
+imap_max_line_length = 2 M
+auth_cache_verify_password_with_worker = yes
+auth_cache_negative_ttl = 60s
+auth_cache_ttl = 300s
+auth_cache_size = 10M
+auth_verbose_passwords = sha1:6
+service replicator {
+  process_min_avail = 1
+}
+service aggregator {
+  fifo_listener replication-notify-fifo {
+    user = vmail
+  }
+  unix_listener replication-notify {
+    user = vmail
+  }
+}
+service replicator {
+  unix_listener replicator-doveadm {
+    mode = 0666
+  }
+}
+replication_max_conns = 10
+doveadm_port = 12345
+replication_dsync_parameters = -d -l 30 -U -n INBOX
+
+{% include 'sogo_trusted_ip.conf.j2' %}
+{% include 'shared_namespace.conf.j2' %}
+{% include 'fts.conf.j2' %}
+{% include 'sni.conf.j2' %}
+
+# <Includes>
+!include_try /etc/dovecot/extra.conf
+# </Includes>
+
+default_client_limit = 10400
+default_vsz_limit = 1024 M

+ 308 - 308
data/conf/dovecot/dovecot.folders.conf → data/conf/dovecot/config_templates/dovecot.folders.conf.j2

@@ -1,308 +1,308 @@
-namespace inbox {
-  inbox = yes
-  location =
-  separator = /
-  mailbox "Trash" {
-    auto = subscribe
-    special_use = \Trash
-  }
-  mailbox "Deleted Messages" {
-    special_use = \Trash
-  }
-  mailbox "Deleted Items" {
-    special_use = \Trash
-  }
-  mailbox "Rubbish" {
-    special_use = \Trash
-  }
-  mailbox "Gelöschte Objekte" {
-    special_use = \Trash
-  }
-  mailbox "Gelöschte Elemente" {
-    special_use = \Trash
-  }
-  mailbox "Papierkorb" {
-    special_use = \Trash
-  }
-  mailbox "Itens Excluidos" {
-    special_use = \Trash
-  }
-  mailbox "Itens Excluídos" {
-    special_use = \Trash
-  }
-  mailbox "Lixeira" {
-    special_use = \Trash
-  }
-  mailbox "Prullenbak" {
-    special_use = \Trash
-  }
-  mailbox "Odstránené položky" {
-    special_use = \Trash
-  }
-  mailbox "Koš" {
-    special_use = \Trash
-  }
-  mailbox "Verwijderde items" {
-    special_use = \Trash
-  }
-  mailbox "Удаленные" {
-    special_use = \Trash
-  }
-  mailbox "Удаленные элементы" {
-    special_use = \Trash
-  }
-  mailbox "Корзина" {
-    special_use = \Trash
-  }
-  mailbox "Видалені" {
-    special_use = \Trash
-  }
-  mailbox "Видалені елементи" {
-    special_use = \Trash
-  }
-  mailbox "Кошик" {
-    special_use = \Trash
-  }
-  mailbox "废件箱" {
-    special_use = \Trash
-  }
-  mailbox "已删除消息" {
-    special_use = \Trash
-  }
-  mailbox "已删除邮件" {
-    special_use = \Trash
-  }
-  mailbox "Archive" {
-    auto = subscribe
-    special_use = \Archive
-  }
-  mailbox "Archiv" {
-    special_use = \Archive
-  }
-  mailbox "Archives" {
-    special_use = \Archive
-  }
-  mailbox "Arquivo" {
-    special_use = \Archive
-  }
-  mailbox "Arquivos" {
-    special_use = \Archive
-  }
-  mailbox "Archief" {
-    special_use = \Archive
-  }
-  mailbox "Archív" {
-    special_use = \Archive
-  }
-  mailbox "Archivovať" {
-    special_use = \Archive
-  }
-  mailbox "归档" {
-    special_use = \Archive
-  }
-  mailbox "Архив" {
-    special_use = \Archive
-  }
-  mailbox "Архів" {
-    special_use = \Archive
-  }
-  mailbox "Sent" {
-    auto = subscribe
-    special_use = \Sent
-  }
-  mailbox "Sent Messages" {
-    special_use = \Sent
-  }
-  mailbox "Sent Items" {
-    special_use = \Sent
-  }
-  mailbox "已发送" {
-    special_use = \Sent
-  }
-  mailbox "已发送消息" {
-    special_use = \Sent
-  }
-  mailbox "已发送邮件" {
-    special_use = \Sent
-  }
-  mailbox "Отправленные" {
-    special_use = \Sent
-  }
-  mailbox "Отправленные элементы" {
-    special_use = \Sent
-  }
-  mailbox "Надіслані" {
-    special_use = \Sent
-  }
-  mailbox "Надіслані елементи" {
-    special_use = \Sent
-  }
-  mailbox "Gesendet" {
-    special_use = \Sent
-  }
-  mailbox "Gesendete Objekte" {
-    special_use = \Sent
-  }
-  mailbox "Gesendete Elemente" {
-    special_use = \Sent
-  }
-  mailbox "Itens Enviados" {
-    special_use = \Sent
-  }
-  mailbox "Enviados" {
-    special_use = \Sent
-  }
-  mailbox "Verzonden items" {
-    special_use = \Sent
-  }
-  mailbox "Verzonden" {
-    special_use = \Sent
-  }
-  mailbox "Odoslaná pošta" {
-    special_use = \Sent
-  }
-  mailbox "Odoslané" {
-    special_use = \Sent
-  }
-  mailbox "Drafts" {
-    auto = subscribe
-    special_use = \Drafts
-  }
-  mailbox "Entwürfe" {
-    special_use = \Drafts
-  }
-  mailbox "Rascunhos" {
-    special_use = \Drafts
-  }
-  mailbox "Concepten" {
-    special_use = \Drafts
-  }
-  mailbox "Koncepty" {
-    special_use = \Drafts
-  }
-  mailbox "草稿" {
-    special_use = \Drafts
-  }
-  mailbox "草稿箱" {
-    special_use = \Drafts
-  }
-  mailbox "Черновики" {
-    special_use = \Drafts
-  }
-  mailbox "Чернетки" {
-    special_use = \Drafts
-  }
-  mailbox "Junk" {
-    auto = subscribe
-    special_use = \Junk
-  }
-  mailbox "Junk-E-Mail" {
-    special_use = \Junk
-  }
-  mailbox "Junk E-Mail" {
-    special_use = \Junk
-  }
-  mailbox "Spam" {
-    special_use = \Junk
-  }
-  mailbox "Lixo Eletrônico" {
-    special_use = \Junk
-  }
-  mailbox "Nevyžiadaná pošta" {
-    special_use = \Junk
-  }
-  mailbox "Infikované položky" {
-    special_use = \Junk
-  }
-  mailbox "Ongewenste e-mail" {
-    special_use = \Junk
-  }
-  mailbox "垃圾" {
-    special_use = \Junk
-  }
-  mailbox "垃圾箱" {
-    special_use = \Junk
-  }
-  mailbox "Нежелательная почта" {
-    special_use = \Junk
-  }
-  mailbox "Спам" {
-    special_use = \Junk
-  }
-  mailbox "Небажана пошта" {
-    special_use = \Junk
-  }
-  mailbox "Koncepty" {
-    special_use = \Drafts
-  }
-  mailbox "Nevyžádaná pošta" {
-    special_use = \Junk
-  }
-  mailbox "Odstraněná pošta" {
-    special_use = \Trash
-  }
-  mailbox "Odeslaná pošta" {
-    special_use = \Sent
-  }
-  mailbox "Skräp" {
-    special_use = \Trash
-  }
-  mailbox "Borttagna Meddelanden" {
-    special_use = \Trash
-  }
-  mailbox "Arkiv" {
-    special_use = \Archive
-  }
-  mailbox "Arkeverat" {
-    special_use = \Archive
-  }
-  mailbox "Skickat" {
-    special_use = \Sent
-  }
-  mailbox "Skickade Meddelanden" {
-    special_use = \Sent
-  }
-  mailbox "Utkast" {
-    special_use = \Drafts
-  }
-  mailbox "Skraldespand" {
-    special_use = \Trash
-  }
-  mailbox "Slettet mails" {
-    special_use = \Trash
-  }
-  mailbox "Arkiv" {
-    special_use = \Archive
-  }
-  mailbox "Arkiveret mails" {
-    special_use = \Archive
-  }
-  mailbox "Sendt" {
-    special_use = \Sent
-  }
-  mailbox "Sendte mails" {
-    special_use = \Sent
-  }
-  mailbox "Udkast" {
-    special_use = \Drafts
-  }
-  mailbox "Kladde" {
-    special_use = \Drafts
-  }
-  mailbox "Πρόχειρα" {
-    special_use = \Drafts
-  }
-  mailbox "Απεσταλμένα" {
-    special_use = \Sent
-  }
-  mailbox "Κάδος απορριμάτων" {
-    special_use = \Trash
-  }
-  mailbox "Ανεπιθύμητα" {
-    special_use = \Junk
-  }
-  mailbox "Αρχειοθετημένα" {
-    special_use = \Archive
-  }
-  prefix =
-}
+namespace inbox {
+  inbox = yes
+  location =
+  separator = /
+  mailbox "Trash" {
+    auto = subscribe
+    special_use = \Trash
+  }
+  mailbox "Deleted Messages" {
+    special_use = \Trash
+  }
+  mailbox "Deleted Items" {
+    special_use = \Trash
+  }
+  mailbox "Rubbish" {
+    special_use = \Trash
+  }
+  mailbox "Gelöschte Objekte" {
+    special_use = \Trash
+  }
+  mailbox "Gelöschte Elemente" {
+    special_use = \Trash
+  }
+  mailbox "Papierkorb" {
+    special_use = \Trash
+  }
+  mailbox "Itens Excluidos" {
+    special_use = \Trash
+  }
+  mailbox "Itens Excluídos" {
+    special_use = \Trash
+  }
+  mailbox "Lixeira" {
+    special_use = \Trash
+  }
+  mailbox "Prullenbak" {
+    special_use = \Trash
+  }
+  mailbox "Odstránené položky" {
+    special_use = \Trash
+  }
+  mailbox "Koš" {
+    special_use = \Trash
+  }
+  mailbox "Verwijderde items" {
+    special_use = \Trash
+  }
+  mailbox "Удаленные" {
+    special_use = \Trash
+  }
+  mailbox "Удаленные элементы" {
+    special_use = \Trash
+  }
+  mailbox "Корзина" {
+    special_use = \Trash
+  }
+  mailbox "Видалені" {
+    special_use = \Trash
+  }
+  mailbox "Видалені елементи" {
+    special_use = \Trash
+  }
+  mailbox "Кошик" {
+    special_use = \Trash
+  }
+  mailbox "废件箱" {
+    special_use = \Trash
+  }
+  mailbox "已删除消息" {
+    special_use = \Trash
+  }
+  mailbox "已删除邮件" {
+    special_use = \Trash
+  }
+  mailbox "Archive" {
+    auto = subscribe
+    special_use = \Archive
+  }
+  mailbox "Archiv" {
+    special_use = \Archive
+  }
+  mailbox "Archives" {
+    special_use = \Archive
+  }
+  mailbox "Arquivo" {
+    special_use = \Archive
+  }
+  mailbox "Arquivos" {
+    special_use = \Archive
+  }
+  mailbox "Archief" {
+    special_use = \Archive
+  }
+  mailbox "Archív" {
+    special_use = \Archive
+  }
+  mailbox "Archivovať" {
+    special_use = \Archive
+  }
+  mailbox "归档" {
+    special_use = \Archive
+  }
+  mailbox "Архив" {
+    special_use = \Archive
+  }
+  mailbox "Архів" {
+    special_use = \Archive
+  }
+  mailbox "Sent" {
+    auto = subscribe
+    special_use = \Sent
+  }
+  mailbox "Sent Messages" {
+    special_use = \Sent
+  }
+  mailbox "Sent Items" {
+    special_use = \Sent
+  }
+  mailbox "已发送" {
+    special_use = \Sent
+  }
+  mailbox "已发送消息" {
+    special_use = \Sent
+  }
+  mailbox "已发送邮件" {
+    special_use = \Sent
+  }
+  mailbox "Отправленные" {
+    special_use = \Sent
+  }
+  mailbox "Отправленные элементы" {
+    special_use = \Sent
+  }
+  mailbox "Надіслані" {
+    special_use = \Sent
+  }
+  mailbox "Надіслані елементи" {
+    special_use = \Sent
+  }
+  mailbox "Gesendet" {
+    special_use = \Sent
+  }
+  mailbox "Gesendete Objekte" {
+    special_use = \Sent
+  }
+  mailbox "Gesendete Elemente" {
+    special_use = \Sent
+  }
+  mailbox "Itens Enviados" {
+    special_use = \Sent
+  }
+  mailbox "Enviados" {
+    special_use = \Sent
+  }
+  mailbox "Verzonden items" {
+    special_use = \Sent
+  }
+  mailbox "Verzonden" {
+    special_use = \Sent
+  }
+  mailbox "Odoslaná pošta" {
+    special_use = \Sent
+  }
+  mailbox "Odoslané" {
+    special_use = \Sent
+  }
+  mailbox "Drafts" {
+    auto = subscribe
+    special_use = \Drafts
+  }
+  mailbox "Entwürfe" {
+    special_use = \Drafts
+  }
+  mailbox "Rascunhos" {
+    special_use = \Drafts
+  }
+  mailbox "Concepten" {
+    special_use = \Drafts
+  }
+  mailbox "Koncepty" {
+    special_use = \Drafts
+  }
+  mailbox "草稿" {
+    special_use = \Drafts
+  }
+  mailbox "草稿箱" {
+    special_use = \Drafts
+  }
+  mailbox "Черновики" {
+    special_use = \Drafts
+  }
+  mailbox "Чернетки" {
+    special_use = \Drafts
+  }
+  mailbox "Junk" {
+    auto = subscribe
+    special_use = \Junk
+  }
+  mailbox "Junk-E-Mail" {
+    special_use = \Junk
+  }
+  mailbox "Junk E-Mail" {
+    special_use = \Junk
+  }
+  mailbox "Spam" {
+    special_use = \Junk
+  }
+  mailbox "Lixo Eletrônico" {
+    special_use = \Junk
+  }
+  mailbox "Nevyžiadaná pošta" {
+    special_use = \Junk
+  }
+  mailbox "Infikované položky" {
+    special_use = \Junk
+  }
+  mailbox "Ongewenste e-mail" {
+    special_use = \Junk
+  }
+  mailbox "垃圾" {
+    special_use = \Junk
+  }
+  mailbox "垃圾箱" {
+    special_use = \Junk
+  }
+  mailbox "Нежелательная почта" {
+    special_use = \Junk
+  }
+  mailbox "Спам" {
+    special_use = \Junk
+  }
+  mailbox "Небажана пошта" {
+    special_use = \Junk
+  }
+  mailbox "Koncepty" {
+    special_use = \Drafts
+  }
+  mailbox "Nevyžádaná pošta" {
+    special_use = \Junk
+  }
+  mailbox "Odstraněná pošta" {
+    special_use = \Trash
+  }
+  mailbox "Odeslaná pošta" {
+    special_use = \Sent
+  }
+  mailbox "Skräp" {
+    special_use = \Trash
+  }
+  mailbox "Borttagna Meddelanden" {
+    special_use = \Trash
+  }
+  mailbox "Arkiv" {
+    special_use = \Archive
+  }
+  mailbox "Arkeverat" {
+    special_use = \Archive
+  }
+  mailbox "Skickat" {
+    special_use = \Sent
+  }
+  mailbox "Skickade Meddelanden" {
+    special_use = \Sent
+  }
+  mailbox "Utkast" {
+    special_use = \Drafts
+  }
+  mailbox "Skraldespand" {
+    special_use = \Trash
+  }
+  mailbox "Slettet mails" {
+    special_use = \Trash
+  }
+  mailbox "Arkiv" {
+    special_use = \Archive
+  }
+  mailbox "Arkiveret mails" {
+    special_use = \Archive
+  }
+  mailbox "Sendt" {
+    special_use = \Sent
+  }
+  mailbox "Sendte mails" {
+    special_use = \Sent
+  }
+  mailbox "Udkast" {
+    special_use = \Drafts
+  }
+  mailbox "Kladde" {
+    special_use = \Drafts
+  }
+  mailbox "Πρόχειρα" {
+    special_use = \Drafts
+  }
+  mailbox "Απεσταλμένα" {
+    special_use = \Sent
+  }
+  mailbox "Κάδος απορριμάτων" {
+    special_use = \Trash
+  }
+  mailbox "Ανεπιθύμητα" {
+    special_use = \Junk
+  }
+  mailbox "Αρχειοθετημένα" {
+    special_use = \Archive
+  }
+  prefix =
+}

+ 4 - 7
data/conf/dovecot/conf.d/fts.conf → data/conf/dovecot/config_templates/fts.conf.j2

@@ -1,4 +1,4 @@
-# mailcow FTS Flatcurve Settings, change them as you like.
+{% if SKIP_FTS|lower in ['n', 'no'] %}
 plugin {
     fts_autoindex = yes
     fts_autoindex_exclude = \Junk
@@ -24,14 +24,11 @@ plugin {
     fts_index_timeout = 300s
 }
 
-### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
-
 service indexer-worker {
   # Max amount of simultaniously running indexer jobs.
-  process_limit=1
+  process_limit = {{ FTS_PROCS }}
 
   # Max amount of RAM used by EACH indexer process.
-  vsz_limit=128 MB
+  vsz_limit = {{ FTS_HEAP }} MB
 }
-
-### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
+{% endif %}

+ 0 - 0
data/conf/dovecot/global_sieve_after → data/conf/dovecot/config_templates/global_sieve_after.sieve.j2


+ 0 - 0
data/conf/dovecot/global_sieve_before → data/conf/dovecot/config_templates/global_sieve_before.sieve.j2


+ 5 - 0
data/conf/dovecot/config_templates/mail_plugins.j2

@@ -0,0 +1,5 @@
+{%- if SKIP_FTS|lower in ["y", "yes"] -%}
+quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge
+{%- else -%}
+quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge
+{%- endif -%}

+ 5 - 0
data/conf/dovecot/config_templates/mail_plugins_imap.j2

@@ -0,0 +1,5 @@
+{%- if SKIP_FTS|lower in ["y", "yes"] -%}
+quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log
+{%- else -%}
+quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication
+{%- endif -%}

+ 5 - 0
data/conf/dovecot/config_templates/mail_plugins_lmtp.j2

@@ -0,0 +1,5 @@
+{%- if SKIP_FTS|lower in ["y", "yes"] -%}
+quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication
+{%- else -%}
+quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication
+{%- endif -%}

+ 3 - 0
data/conf/dovecot/config_templates/mail_replica.conf.j2

@@ -0,0 +1,3 @@
+{% if MAILCOW_REPLICA_IP and DOVEADM_REPLICA_PORT %}
+mail_replica = tcp:{{ MAILCOW_REPLICA_IP }}:{{ DOVEADM_REPLICA_PORT }}
+{% endif %}

+ 2 - 0
data/conf/dovecot/config_templates/maildir_gc.sh.j2

@@ -0,0 +1,2 @@
+#!/bin/bash
+[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +{{ MAILDIR_GC_TIME }} -exec rm -r {} \;

+ 9 - 0
data/conf/dovecot/config_templates/shared_namespace.conf.j2

@@ -0,0 +1,9 @@
+{% set MAILDIR_SUB_SHARED = '' if not MAILDIR_SUB else '/' ~ MAILDIR_SUB %}
+namespace {
+  type = shared
+  separator = /
+  prefix = Shared/%%u/
+  location = maildir:%%h{{ MAILDIR_SUB_SHARED }}:INDEX=~{{ MAILDIR_SUB_SHARED }}/Shared/%%u
+  subscriptions = no
+  list = children
+}

+ 1 - 0
data/conf/dovecot/config_templates/sieve.creds.j2

@@ -0,0 +1 @@
+{{ DOVECOT_MASTER_USER or RAND_USER }}@mailcow.local:{{ DOVECOT_MASTER_PASS or RAND_PASS }}

+ 6 - 0
data/conf/dovecot/config_templates/sni.conf.j2

@@ -0,0 +1,6 @@
+{% for domain, path in VALID_CERT_DIRS.items() %}
+local_name "{{ domain }}" {
+  ssl_cert = <{{ path }}/cert.pem
+  ssl_key = <{{ path }}/key.pem
+}
+{% endfor %}

+ 1 - 0
data/conf/dovecot/config_templates/sogo-sso.pass.j2

@@ -0,0 +1 @@
+{{ RAND_PASS2 }}

+ 3 - 0
data/conf/dovecot/config_templates/sogo_trusted_ip.conf.j2

@@ -0,0 +1,3 @@
+remote {{ IPV4_NETWORK }}.248 {
+  disable_plaintext_auth = no
+}

+ 3 - 0
data/conf/dovecot/config_templates/source_env.sh.j2

@@ -0,0 +1,3 @@
+{% for key, value in ENV_VARS.items() %}
+export {{ key }}="{{ value | replace('"', '\\"') }}"
+{% endfor %}

+ 0 - 9
data/conf/dovecot/ldap/passdb.conf

@@ -1,9 +0,0 @@
-#hosts = 1.2.3.4
-#dn = cn=admin,dc=example,dc=local
-#dnpass = password
-#ldap_version = 3
-#base = ou=People,dc=example,dc=local
-#auth_bind = no
-#pass_filter = (&(objectClass=posixAccount)(mail=%u))
-#pass_attrs = mail=user,userPassword=password
-#default_pass_scheme = SSHA

+ 3 - 1
docker-compose.yml

@@ -252,7 +252,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: ghcr.io/mailcow/dovecot:2.33
+      image: ghcr.io/mailcow/dovecot:nightly-19052025
       depends_on:
         - mysql-mailcow
         - netfilter-mailcow
@@ -267,6 +267,7 @@ services:
         - ./data/assets/ssl:/etc/ssl/mail/:ro,z
         - ./data/conf/sogo/:/etc/sogo/:z
         - ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z
+        - ./data/web/inc/init_db.inc.php:/init_db.inc.php:z
         - vmail-vol-1:/var/vmail
         - vmail-index-vol-1:/var/vmail_index
         - crypt-vol-1:/mail_crypt/
@@ -275,6 +276,7 @@ services:
         - rspamd-vol-1:/var/lib/rspamd
         - mysql-socket-vol-1:/var/run/mysqld/
       environment:
+        - CONTAINER_NAME=dovecot-mailcow
         - DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-}
         - DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-}
         - MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-}