diff --git a/data/Dockerfiles/bootstrap/main.py b/data/Dockerfiles/bootstrap/main.py index e63bc69ff..6b10998ca 100644 --- a/data/Dockerfiles/bootstrap/main.py +++ b/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() diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py b/data/Dockerfiles/bootstrap/modules/BootstrapBase.py index 325bc5c04..46de259d1 100644 --- a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py +++ b/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 \ No newline at end of file + return e + + def sha1_filter(self, value): + return hashlib.sha1(value.encode()).hexdigest() diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapDovecot.py b/data/Dockerfiles/bootstrap/modules/BootstrapDovecot.py new file mode 100644 index 000000000..9a0e510fa --- /dev/null +++ b/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}") diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 9e49d88cc..4037fc725 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/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"] diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index fe2341bfa..bb90f3c53 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 +if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then + echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Dovecot." + exit $BOOTSTRAP_EXIT_CODE 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 -fi - -exec "$@" +echo "Bootstrap succeeded. Starting Dovecot..." +/usr/sbin/dovecot -F diff --git a/data/Dockerfiles/dovecot/maildir_gc.sh b/data/Dockerfiles/dovecot/maildir_gc.sh deleted file mode 100755 index 21358ccbd..000000000 --- a/data/Dockerfiles/dovecot/maildir_gc.sh +++ /dev/null @@ -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 {} \; diff --git a/data/Dockerfiles/dovecot/quota_notify.py b/data/Dockerfiles/dovecot/quota_notify.py index 598134e22..21d455412 100755 --- a/data/Dockerfiles/dovecot/quota_notify.py +++ b/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]) diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 5b0050002..2573e971c 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/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 diff --git a/data/Dockerfiles/nginx/Dockerfile b/data/Dockerfiles/nginx/Dockerfile index a07c30ae8..d64a96062 100644 --- a/data/Dockerfiles/nginx/Dockerfile +++ b/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 diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index bf2d923ee..b857cbf38 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/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 diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 4cfd1dc5d..2a383ec56 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/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 diff --git a/data/conf/dovecot/config_templates/cron.creds.j2 b/data/conf/dovecot/config_templates/cron.creds.j2 new file mode 100644 index 000000000..8af07ceb2 --- /dev/null +++ b/data/conf/dovecot/config_templates/cron.creds.j2 @@ -0,0 +1 @@ +{{ RAND_USER }}@mailcow.local:{{ RAND_PASS2 }} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/dovecot-dict-sql-quota.conf.j2 b/data/conf/dovecot/config_templates/dovecot-dict-sql-quota.conf.j2 new file mode 100644 index 000000000..228c14c81 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_after.conf.j2 b/data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_after.conf.j2 new file mode 100644 index 000000000..ed1a99c37 --- /dev/null +++ b/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 + } +} diff --git a/data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_before.conf.j2 b/data/conf/dovecot/config_templates/dovecot-dict-sql-sieve_before.conf.j2 new file mode 100644 index 000000000..ea20a89c1 --- /dev/null +++ b/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 + } +} diff --git a/data/conf/dovecot/config_templates/dovecot-dict-sql-userdb.conf.j2 b/data/conf/dovecot/config_templates/dovecot-dict-sql-userdb.conf.j2 new file mode 100644 index 000000000..e7b15e3c8 --- /dev/null +++ b/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'; diff --git a/data/conf/dovecot/config_templates/dovecot-master.passwd.j2 b/data/conf/dovecot/config_templates/dovecot-master.passwd.j2 new file mode 100644 index 000000000..3a76c3118 --- /dev/null +++ b/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 }}:::::: \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/dovecot-master.userdb.j2 b/data/conf/dovecot/config_templates/dovecot-master.userdb.j2 new file mode 100644 index 000000000..d1487d7ff --- /dev/null +++ b/data/conf/dovecot/config_templates/dovecot-master.userdb.j2 @@ -0,0 +1 @@ +{{ DOVECOT_MASTER_USER or RAND_USER }}@mailcow.local::5000:5000:::: \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/dovecot.conf.j2 b/data/conf/dovecot/config_templates/dovecot.conf.j2 new file mode 100644 index 000000000..a0156f098 --- /dev/null +++ b/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 = +!include_try /etc/dovecot/extra.conf +# + +default_client_limit = 10400 +default_vsz_limit = 1024 M diff --git a/data/conf/dovecot/dovecot.folders.conf b/data/conf/dovecot/config_templates/dovecot.folders.conf.j2 similarity index 95% rename from data/conf/dovecot/dovecot.folders.conf rename to data/conf/dovecot/config_templates/dovecot.folders.conf.j2 index fa6872679..b30899f3b 100644 --- a/data/conf/dovecot/dovecot.folders.conf +++ b/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 = +} diff --git a/data/conf/dovecot/conf.d/fts.conf b/data/conf/dovecot/config_templates/fts.conf.j2 similarity index 79% rename from data/conf/dovecot/conf.d/fts.conf rename to data/conf/dovecot/config_templates/fts.conf.j2 index e8a2f73f5..5d967b0dd 100644 --- a/data/conf/dovecot/conf.d/fts.conf +++ b/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! ### \ No newline at end of file +{% endif %} \ No newline at end of file diff --git a/data/conf/dovecot/global_sieve_after b/data/conf/dovecot/config_templates/global_sieve_after.sieve.j2 similarity index 100% rename from data/conf/dovecot/global_sieve_after rename to data/conf/dovecot/config_templates/global_sieve_after.sieve.j2 diff --git a/data/conf/dovecot/global_sieve_before b/data/conf/dovecot/config_templates/global_sieve_before.sieve.j2 similarity index 100% rename from data/conf/dovecot/global_sieve_before rename to data/conf/dovecot/config_templates/global_sieve_before.sieve.j2 diff --git a/data/conf/dovecot/config_templates/mail_plugins.j2 b/data/conf/dovecot/config_templates/mail_plugins.j2 new file mode 100644 index 000000000..9e4eb1148 --- /dev/null +++ b/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 -%} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/mail_plugins_imap.j2 b/data/conf/dovecot/config_templates/mail_plugins_imap.j2 new file mode 100644 index 000000000..dcb2bc55e --- /dev/null +++ b/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 -%} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/mail_plugins_lmtp.j2 b/data/conf/dovecot/config_templates/mail_plugins_lmtp.j2 new file mode 100644 index 000000000..3a0fcf2fb --- /dev/null +++ b/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 -%} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/mail_replica.conf.j2 b/data/conf/dovecot/config_templates/mail_replica.conf.j2 new file mode 100644 index 000000000..d151ce1c9 --- /dev/null +++ b/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 %} diff --git a/data/conf/dovecot/config_templates/maildir_gc.sh.j2 b/data/conf/dovecot/config_templates/maildir_gc.sh.j2 new file mode 100644 index 000000000..557123186 --- /dev/null +++ b/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 {} \; diff --git a/data/conf/dovecot/config_templates/shared_namespace.conf.j2 b/data/conf/dovecot/config_templates/shared_namespace.conf.j2 new file mode 100644 index 000000000..eebb55fa1 --- /dev/null +++ b/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 +} diff --git a/data/conf/dovecot/config_templates/sieve.creds.j2 b/data/conf/dovecot/config_templates/sieve.creds.j2 new file mode 100644 index 000000000..3c4fdefd1 --- /dev/null +++ b/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 }} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/sni.conf.j2 b/data/conf/dovecot/config_templates/sni.conf.j2 new file mode 100644 index 000000000..24cd9f6ec --- /dev/null +++ b/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 %} diff --git a/data/conf/dovecot/config_templates/sogo-sso.pass.j2 b/data/conf/dovecot/config_templates/sogo-sso.pass.j2 new file mode 100644 index 000000000..4f166cf51 --- /dev/null +++ b/data/conf/dovecot/config_templates/sogo-sso.pass.j2 @@ -0,0 +1 @@ +{{ RAND_PASS2 }} \ No newline at end of file diff --git a/data/conf/dovecot/config_templates/sogo_trusted_ip.conf.j2 b/data/conf/dovecot/config_templates/sogo_trusted_ip.conf.j2 new file mode 100644 index 000000000..9a7d4457d --- /dev/null +++ b/data/conf/dovecot/config_templates/sogo_trusted_ip.conf.j2 @@ -0,0 +1,3 @@ +remote {{ IPV4_NETWORK }}.248 { + disable_plaintext_auth = no +} diff --git a/data/conf/dovecot/config_templates/source_env.sh.j2 b/data/conf/dovecot/config_templates/source_env.sh.j2 new file mode 100644 index 000000000..7df431f0c --- /dev/null +++ b/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 %} diff --git a/data/conf/dovecot/ldap/passdb.conf b/data/conf/dovecot/ldap/passdb.conf deleted file mode 100644 index 12fc3c050..000000000 --- a/data/conf/dovecot/ldap/passdb.conf +++ /dev/null @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 9750beb92..696820374 100644 --- a/docker-compose.yml +++ b/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:-}