Forráskód Böngészése

[Postfix] use python bootstrapper to start POSTFIX container

FreddleSpl0it 3 hónapja
szülő
commit
5a097ed5f7
44 módosított fájl, 751 hozzáadás és 554 törlés
  1. 2 0
      data/Dockerfiles/bootstrap/main.py
  2. 82 0
      data/Dockerfiles/bootstrap/modules/BootstrapBase.py
  3. 3 3
      data/Dockerfiles/bootstrap/modules/BootstrapNginx.py
  4. 115 0
      data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py
  5. 2 2
      data/Dockerfiles/bootstrap/modules/BootstrapSogo.py
  6. 16 13
      data/Dockerfiles/postfix/Dockerfile
  7. 18 4
      data/Dockerfiles/postfix/docker-entrypoint.sh
  8. 0 527
      data/Dockerfiles/postfix/postfix.sh
  9. 2 2
      data/Dockerfiles/postfix/supervisord.conf
  10. 1 1
      data/Dockerfiles/sogo/docker-entrypoint.sh
  11. 0 0
      data/conf/nginx/config_templates/listen_plain.active.j2
  12. 0 0
      data/conf/nginx/config_templates/listen_ssl.active.j2
  13. 0 0
      data/conf/nginx/config_templates/nginx.conf.j2
  14. 0 0
      data/conf/nginx/config_templates/server_name.active.j2
  15. 0 0
      data/conf/nginx/config_templates/sites-default.conf.j2
  16. 4 0
      data/conf/postfix/config_templates/aliases.j2
  17. 4 0
      data/conf/postfix/config_templates/custom_postscreen_whitelist.cidr.j2
  18. 0 0
      data/conf/postfix/config_templates/custom_transport.pcre.j2
  19. 35 0
      data/conf/postfix/config_templates/dns_blocklists.cf.j2
  20. 8 0
      data/conf/postfix/config_templates/dns_reply.map.j2
  21. 179 0
      data/conf/postfix/config_templates/main.cf.j2
  22. 8 0
      data/conf/postfix/config_templates/mysql_mbr_access_maps.cf.j2
  23. 8 0
      data/conf/postfix/config_templates/mysql_recipient_bcc_maps.cf.j2
  24. 7 0
      data/conf/postfix/config_templates/mysql_recipient_canonical_maps.cf.j2
  25. 13 0
      data/conf/postfix/config_templates/mysql_relay_ne.cf.j2
  26. 15 0
      data/conf/postfix/config_templates/mysql_relay_recipient_maps.cf.j2
  27. 34 0
      data/conf/postfix/config_templates/mysql_sasl_passwd_maps_sender_dependent.cf.j2
  28. 9 0
      data/conf/postfix/config_templates/mysql_sasl_passwd_maps_transport_maps.cf.j2
  29. 8 0
      data/conf/postfix/config_templates/mysql_sender_bcc_maps.cf.j2
  30. 43 0
      data/conf/postfix/config_templates/mysql_sender_dependent_default_transport_maps.cf.j2
  31. 14 0
      data/conf/postfix/config_templates/mysql_tls_enforce_in_policy.cf.j2
  32. 5 0
      data/conf/postfix/config_templates/mysql_tls_policy_override_maps.cf.j2
  33. 7 0
      data/conf/postfix/config_templates/mysql_transport_maps.cf.j2
  34. 9 0
      data/conf/postfix/config_templates/mysql_virtual_alias_domain_maps.cf.j2
  35. 7 0
      data/conf/postfix/config_templates/mysql_virtual_alias_maps.cf.j2
  36. 10 0
      data/conf/postfix/config_templates/mysql_virtual_domains_maps.cf.j2
  37. 5 0
      data/conf/postfix/config_templates/mysql_virtual_mailbox_maps.cf.j2
  38. 5 0
      data/conf/postfix/config_templates/mysql_virtual_relay_domain_maps.cf.j2
  39. 6 0
      data/conf/postfix/config_templates/mysql_virtual_resource_maps.cf.j2
  40. 50 0
      data/conf/postfix/config_templates/mysql_virtual_sender_acl.cf.j2
  41. 7 0
      data/conf/postfix/config_templates/mysql_virtual_spamalias_maps.cf.j2
  42. 7 0
      data/conf/postfix/config_templates/sni.map.j2
  43. 0 0
      data/conf/postfix/dns_reply.map
  44. 3 2
      docker-compose.yml

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

@@ -8,6 +8,8 @@ def main():
     from modules.BootstrapSogo import Bootstrap
   elif container_name == "nginx-mailcow":
     from modules.BootstrapNginx import Bootstrap
+  elif container_name == "postfix-mailcow":
+    from modules.BootstrapPostfix import Bootstrap
   else:
     print(f"No bootstrap handler for container: {container_name}", file=sys.stderr)
     sys.exit(1)

+ 82 - 0
data/Dockerfiles/bootstrap/modules/BootstrapBase.py

@@ -231,6 +231,21 @@ class BootstrapBase:
 
     shutil.move(str(src_path), str(dst_path))
 
+  def create_dir(self, path):
+    """
+    Creates a directory if it does not exist.
+
+    If the directory is missing, it will be created along with any necessary parent directories.
+
+    Args:
+      path (str or Path): The directory path to create.
+    """
+
+    dir_path = Path(path)
+    if not dir_path.exists():
+      print(f"Creating directory: {dir_path}")
+      dir_path.mkdir(parents=True, exist_ok=True)
+
   def patch_exists(self, target_file, patch_file, reverse=False):
     """
     Checks whether a patch can be applied (or reversed) to a target file.
@@ -414,6 +429,35 @@ class BootstrapBase:
       print(f"Waiting for {host}...")
       time.sleep(retry_interval)
 
+  def wait_for_dns(self, domain, retry_interval=1, timeout=30):
+    """
+    Waits until the domain resolves via DNS using pure Python (socket).
+
+    Args:
+      domain (str): The domain to resolve.
+      retry_interval (int): Time (seconds) to wait between attempts.
+      timeout (int): Maximum total wait time (seconds).
+
+    Returns:
+      bool: True if resolved, False if timed out.
+    """
+
+    start = time.time()
+    while True:
+      try:
+        socket.gethostbyname(domain)
+        print(f"{domain} is resolving via DNS.")
+        return True
+      except socket.gaierror:
+        pass
+
+      if time.time() - start > timeout:
+        print(f"DNS resolution for {domain} timed out.")
+        return False
+
+      print(f"Waiting for DNS for {domain}...")
+      time.sleep(retry_interval)
+
   def _get_current_db_version(self):
     """
     Fetches the current schema version from the database.
@@ -478,3 +522,41 @@ class BootstrapBase:
 
     allowed_chars = string.ascii_letters + string.digits + "_-"
     return ''.join(secrets.choice(allowed_chars) for _ in range(length))
+
+  def run_command(self, command, check=True, shell=False):
+    """
+    Executes an OS command and optionally checks for errors.
+
+    Args:
+      command (str or list): The command to execute. Can be a string (if shell=True)
+                            or a list of command arguments.
+      check (bool): If True, raises CalledProcessError on failure.
+      shell (bool): If True, runs the command in a shell.
+
+    Returns:
+      subprocess.CompletedProcess: The result of the command execution.
+
+    Logs:
+      Prints the command being run and any error output.
+    """
+
+    try:
+      result = subprocess.run(
+        command,
+        shell=shell,
+        check=check,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        text=True
+      )
+      if result.stdout:
+        print(result.stdout.strip())
+      if result.stderr:
+        print(result.stderr.strip())
+      return result
+    except subprocess.CalledProcessError as e:
+      print(f"Command failed with exit code {e.returncode}: {e.cmd}")
+      print(e.stderr.strip())
+      if check:
+        raise
+      return e

+ 3 - 3
data/Dockerfiles/bootstrap/modules/BootstrapNginx.py

@@ -22,10 +22,10 @@ class Bootstrap(BootstrapBase):
 
     # Setup Jinja2 Environment and load vars
     self.env = Environment(
-      loader=FileSystemLoader('./etc/nginx/conf.d/templates'),
+      loader=FileSystemLoader('./etc/nginx/conf.d/config_templates'),
       keep_trailing_newline=True,
-      lstrip_blocks=False,
-      trim_blocks=False
+      lstrip_blocks=True,
+      trim_blocks=True
     )
     extra_vars = {
       "VALID_CERT_DIRS": self.get_valid_cert_dirs(),

+ 115 - 0
data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py

@@ -0,0 +1,115 @@
+from jinja2 import Environment, FileSystemLoader
+from modules.BootstrapBase import BootstrapBase
+from pathlib import Path
+import os
+import sys
+import time
+
+class Bootstrap(BootstrapBase):
+  def bootstrap(self):
+    # Connect to MySQL
+    self.connect_mysql()
+
+    # Wait for DNS
+    self.wait_for_dns("mailcow.email")
+
+    self.create_dir("/opt/postfix/conf/sql/")
+
+    # Setup Jinja2 Environment and load vars
+    self.env = Environment(
+      loader=FileSystemLoader('./opt/postfix/conf/config_templates'),
+      keep_trailing_newline=True,
+      lstrip_blocks=True,
+      trim_blocks=True
+    )
+    with open("/opt/postfix/conf/extra.cf", "r") as f:
+      extra_config = f.read()
+    extra_vars = {
+      "VALID_CERT_DIRS": self.get_valid_cert_dirs(),
+      "EXTRA_CF": extra_config
+    }
+    self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars)
+
+    print("Set Timezone")
+    self.set_timezone()
+
+    print("Set Syslog redis")
+    self.set_syslog_redis()
+
+    print("Render config")
+    self.render_config("aliases.j2", "/etc/aliases")
+    self.render_config("mysql_relay_ne.cf.j2", "/opt/postfix/conf/sql/mysql_relay_ne.cf")
+    self.render_config("mysql_relay_recipient_maps.cf.j2", "/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf")
+    self.render_config("mysql_tls_policy_override_maps.cf.j2", "/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf")
+    self.render_config("mysql_tls_enforce_in_policy.cf.j2", "/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf")
+    self.render_config("mysql_sender_dependent_default_transport_maps.cf.j2", "/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf")
+    self.render_config("mysql_transport_maps.cf.j2", "/opt/postfix/conf/sql/mysql_transport_maps.cf")
+    self.render_config("mysql_virtual_resource_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf")
+    self.render_config("mysql_sasl_passwd_maps_sender_dependent.cf.j2", "/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf")
+    self.render_config("mysql_sasl_passwd_maps_transport_maps.cf.j2", "/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf")
+    self.render_config("mysql_virtual_alias_domain_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf")
+    self.render_config("mysql_virtual_alias_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf")
+    self.render_config("mysql_recipient_bcc_maps.cf.j2", "/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf")
+    self.render_config("mysql_sender_bcc_maps.cf.j2", "/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf")
+    self.render_config("mysql_recipient_canonical_maps.cf.j2", "/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf")
+    self.render_config("mysql_virtual_domains_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf")
+    self.render_config("mysql_virtual_mailbox_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf")
+    self.render_config("mysql_virtual_relay_domain_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf")
+    self.render_config("mysql_virtual_sender_acl.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf")
+    self.render_config("mysql_mbr_access_maps.cf.j2", "/opt/postfix/conf/sql/mysql_mbr_access_maps.cf")
+    self.render_config("mysql_virtual_spamalias_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf")
+    self.render_config("sni.map.j2", "/opt/postfix/conf/sni.map")
+    self.render_config("main.cf.j2", "/opt/postfix/conf/main.cf")
+
+    # Conditional render
+    if not Path("/opt/postfix/conf/dns_blocklists.cf").exists():
+      self.render_config("dns_blocklists.cf.j2", "/opt/postfix/conf/dns_blocklists.cf")
+    if not Path("/opt/postfix/conf/dns_reply.map").exists():
+      self.render_config("dns_reply.map.j2", "/opt/postfix/conf/dns_reply.map")
+    if not Path("/opt/postfix/conf/custom_postscreen_whitelist.cidr").exists():
+      self.render_config("custom_postscreen_whitelist.cidr.j2", "/opt/postfix/conf/custom_postscreen_whitelist.cidr")
+    if not Path("/opt/postfix/conf/custom_transport.pcre").exists():
+      self.render_config("custom_transport.pcre.j2", "/opt/postfix/conf/custom_transport.pcre")
+
+    # Create SNI Config
+    self.run_command(["postmap", "-F", "hash:/opt/postfix/conf/sni.map"])
+
+    # Fix Postfix permissions
+    self.set_owner("/opt/postfix/conf/sql", user="root", group="postfix", recursive=True)
+    self.set_owner("/opt/postfix/conf/custom_transport.pcre", user="root", group="postfix")
+    for cf_file in Path("/opt/postfix/conf/sql").glob("*.cf"):
+      self.set_permissions(cf_file, 0o640)
+    self.set_permissions("/opt/postfix/conf/custom_transport.pcre", 0o640)
+    self.set_owner("/var/spool/postfix/public", user="root", group="postdrop", recursive=True)
+    self.set_owner("/var/spool/postfix/maildrop", user="root", group="postdrop", recursive=True)
+    self.run_command(["postfix", "set-permissions"], check=False)
+
+    # Checking if there is a leftover of a crashed postfix container before starting a new one
+    pid_file = Path("/var/spool/postfix/pid/master.pid")
+    if pid_file.exists():
+      print(f"Removing stale Postfix PID file: {pid_file}")
+      pid_file.unlink()
+
+  def get_valid_cert_dirs(self):
+    certs = {}
+    base_path = Path("/etc/ssl/mail")
+    if not base_path.exists():
+      return certs
+
+    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()]
+        if domains:
+          certs[str(cert_dir)] = domains
+
+    return certs

+ 2 - 2
data/Dockerfiles/bootstrap/modules/BootstrapSogo.py

@@ -29,8 +29,8 @@ class Bootstrap(BootstrapBase):
     self.env = Environment(
       loader=FileSystemLoader("./etc/sogo/config_templates"),
       keep_trailing_newline=True,
-      lstrip_blocks=False,
-      trim_blocks=False
+      lstrip_blocks=True,
+      trim_blocks=True
     )
     extra_vars = {
       "SQL_DOMAINS": self.get_domains(),

+ 16 - 13
data/Dockerfiles/postfix/Dockerfile

@@ -34,23 +34,27 @@ RUN groupadd -g 102 postfix \
 	syslog-ng-core \
 	syslog-ng-mod-redis \
   	tzdata \
+	python3 python3-pip \
 	&& rm -rf /var/lib/apt/lists/* \
 	&& touch /etc/default/locale \
   && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
   && chmod +x /usr/local/sbin/postconf
 
-COPY supervisord.conf /etc/supervisor/supervisord.conf
-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 postfix.sh /opt/postfix.sh
-COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
-COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
-COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
-COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
-COPY docker-entrypoint.sh /docker-entrypoint.sh
-
-RUN chmod +x /opt/postfix.sh \
-  /usr/local/bin/rspamd-pipe-ham \
+RUN pip install  --break-system-packages \
+  mysql-connector-python \
+  jinja2
+
+COPY data/Dockerfiles/bootstrap /bootstrap
+COPY data/Dockerfiles/postfix/supervisord.conf /etc/supervisor/supervisord.conf
+COPY data/Dockerfiles/postfix/syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
+COPY data/Dockerfiles/postfix/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
+COPY data/Dockerfiles/postfix/rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
+COPY data/Dockerfiles/postfix/rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
+COPY data/Dockerfiles/postfix/whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
+COPY data/Dockerfiles/postfix/stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
+COPY data/Dockerfiles/postfix/docker-entrypoint.sh /docker-entrypoint.sh
+
+RUN chmod +x /usr/local/bin/rspamd-pipe-ham \
   /usr/local/bin/rspamd-pipe-spam \
   /usr/local/bin/whitelist_forwardinghosts.sh \
   /usr/local/sbin/stop-supervisor.sh
@@ -58,6 +62,5 @@ RUN rm -rf /tmp/* /var/tmp/*
 
 EXPOSE 588
 
-ENTRYPOINT ["/docker-entrypoint.sh"]
 
 CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

+ 18 - 4
data/Dockerfiles/postfix/docker-entrypoint.sh

@@ -8,8 +8,12 @@ for file in /hooks/*; do
   fi
 done
 
-if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
-  cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
+python3 -u /bootstrap/main.py
+BOOTSTRAP_EXIT_CODE=$?
+
+if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
+  echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Postfix."
+  exit $BOOTSTRAP_EXIT_CODE
 fi
 
 # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
@@ -21,6 +25,16 @@ if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/
     echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
     echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
     echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
-fi  
+fi
 
-exec "$@"
+
+# Start Postfix
+postconf -c /opt/postfix/conf > /dev/null
+if [[ $? != 0 ]]; then
+  echo "Postfix configuration error, refusing to start."
+  exit 1
+else
+  echo "Bootstrap succeeded. Starting Postfix..."
+  postfix -c /opt/postfix/conf start
+  sleep 126144000
+fi

+ 0 - 527
data/Dockerfiles/postfix/postfix.sh

@@ -1,527 +0,0 @@
-#!/bin/bash
-
-trap "postfix stop" EXIT
-
-[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
-
-# 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
-
-cat <<EOF > /etc/aliases
-# Autogenerated by mailcow
-null: /dev/null
-watchdog: /dev/null
-ham: "|/usr/local/bin/rspamd-pipe-ham"
-spam: "|/usr/local/bin/rspamd-pipe-spam"
-EOF
-newaliases;
-
-# create sni configuration
-if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-  echo -n "" > /opt/postfix/conf/sni.map
-else
-  echo -n "" > /opt/postfix/conf/sni.map;
-  for cert_dir in /etc/ssl/mail/*/ ; do
-    if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then
-      continue;
-    fi
-    IFS=" " read -r -a domains <<< "$(cat "${cert_dir}domains")"
-    for domain in "${domains[@]}"; do
-      echo -n "${domain} ${cert_dir}key.pem ${cert_dir}cert.pem" >> /opt/postfix/conf/sni.map;
-      echo "" >> /opt/postfix/conf/sni.map;
-    done
-  done
-fi
-postmap -F hash:/opt/postfix/conf/sni.map;
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_relay_ne.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT IF(EXISTS(SELECT address, domain FROM alias
-      WHERE address = '%s'
-        AND domain IN (
-          SELECT domain FROM domain
-            WHERE backupmx = '1'
-              AND relay_all_recipients = '1'
-              AND relay_unknown_only = '1')
-
-      ), 'lmtp:inet:dovecot:24', NULL) AS 'transport'
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT DISTINCT
-  CASE WHEN '%d' IN (
-    SELECT domain FROM domain
-      WHERE relay_all_recipients=1
-        AND domain='%d'
-        AND backupmx=1
-  )
-  THEN '%s' ELSE (
-    SELECT goto FROM alias WHERE address='%s' AND active='1'
-  )
-  END AS result;
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_override WHERE active = '1' AND dest = '%s'
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT IF(EXISTS(
-  SELECT 'TLS_ACTIVE' FROM alias
-    LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto
-      WHERE (address='%s'
-        OR address IN (
-          SELECT CONCAT('%u', '@', target_domain) FROM alias_domain
-            WHERE alias_domain='%d'
-        )
-      ) AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_in')) = '1' AND mailbox.active = '1'
-  ), 'reject_plaintext_session', NULL) AS 'tls_enforce_in';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps
-  FROM (
-    SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias
-      LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto
-        WHERE (address = '%s'
-          OR address IN (
-            SELECT CONCAT('%u', '@', target_domain) FROM alias_domain
-              WHERE alias_domain = '%d'
-          )
-        )
-        AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_out')) = '1'
-        AND mailbox.active = '1'
-    ), 'smtp_enforced_tls:', 'smtp:') AS 'transport'
-    UNION ALL
-    SELECT COALESCE(
-      (SELECT hostname FROM relayhosts
-      LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
-        WHERE relayhosts.active = '1'
-          AND (
-            mailbox.username IN (SELECT alias.goto from alias
-              JOIN mailbox ON mailbox.username = alias.goto
-                WHERE alias.active = '1'
-                  AND alias.address = '%s'
-                  AND alias.address NOT LIKE '@%%'
-            )
-          )
-      ),
-      (SELECT hostname FROM relayhosts
-      LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
-        WHERE relayhosts.active = '1'
-          AND (domain.domain = '%d'
-            OR domain.domain IN (
-              SELECT target_domain FROM alias_domain
-                WHERE alias_domain = '%d'
-            )
-          )
-      )
-    )
-  ) AS transport_view;
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_transport_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT CONCAT('smtp_via_transport_maps:', nexthop) AS transport FROM transports
-  WHERE active = '1'
-  AND destination = '%s';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_resource_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT 'null@localhost' FROM mailbox
-  WHERE kind REGEXP 'location|thing|group' AND username = '%s';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
-  WHERE id IN (
-    SELECT COALESCE(
-      (SELECT id FROM relayhosts
-      LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
-      WHERE relayhosts.active = '1'
-        AND (domain.domain = '%d'
-          OR domain.domain IN (
-            SELECT target_domain FROM alias_domain
-            WHERE alias_domain = '%d'
-          )
-        )
-      ),
-      (SELECT id FROM relayhosts
-      LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
-      WHERE relayhosts.active = '1'
-        AND (
-          mailbox.username IN (
-            SELECT alias.goto from alias
-              JOIN mailbox ON mailbox.username = alias.goto
-                WHERE alias.active = '1'
-                  AND alias.address = '%s'
-                  AND alias.address NOT LIKE '@%%'
-          )
-        )
-      )
-    )
-  )
-  AND active = '1'
-  AND username != '';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports
-  WHERE nexthop = '%s'
-  AND active = '1'
-  AND username != ''
-  LIMIT 1;
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT username FROM mailbox, alias_domain
-  WHERE alias_domain.alias_domain = '%d'
-    AND mailbox.username = CONCAT('%u', '@', alias_domain.target_domain)
-    AND (mailbox.active = '1' OR mailbox.active = '2')
-    AND alias_domain.active='1'
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT goto FROM alias
-  WHERE address='%s'
-    AND (active='1' OR active='2');
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT bcc_dest FROM bcc_maps
-  WHERE local_dest='%s'
-    AND type='rcpt'
-    AND active='1';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT bcc_dest FROM bcc_maps
-  WHERE local_dest='%s'
-    AND type='sender'
-    AND active='1';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT new_dest FROM recipient_maps
-  WHERE old_dest='%s'
-    AND active='1';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1'
-  UNION
-  SELECT domain FROM domain
-    WHERE domain='%s'
-      AND active = '1'
-      AND backupmx = '0'
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%u/') FROM mailbox WHERE username='%s' AND (active = '1' OR active = '2')
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1'
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-# First select queries domain and alias_domain to determine if domains are active.
-query = SELECT goto FROM alias
-  WHERE id IN (
-      SELECT COALESCE (
-        (
-          SELECT id FROM alias
-            WHERE address='%s'
-            AND (active='1' OR active='2')
-        ), (
-          SELECT id FROM alias
-            WHERE address='@%d'
-            AND (active='1' OR active='2')
-        )
-      )
-    )
-    AND active='1'
-    AND (domain IN
-      (SELECT domain FROM domain
-        WHERE domain='%d'
-          AND active='1')
-      OR domain in (
-        SELECT alias_domain FROM alias_domain
-          WHERE alias_domain='%d'
-            AND active='1'
-      )
-    )
-  UNION
-  SELECT logged_in_as FROM sender_acl
-    WHERE send_as='@%d'
-      OR send_as='%s'
-      OR send_as='*'
-      OR send_as IN (
-        SELECT CONCAT('@',target_domain) FROM alias_domain
-          WHERE alias_domain = '%d')
-      OR send_as IN (
-        SELECT CONCAT('%u','@',target_domain) FROM alias_domain
-          WHERE alias_domain = '%d')
-      AND logged_in_as NOT IN (
-        SELECT goto FROM alias
-          WHERE address='%s')
-  UNION
-  SELECT username FROM mailbox, alias_domain
-    WHERE alias_domain.alias_domain = '%d'
-      AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain)
-      AND (mailbox.active = '1' OR mailbox.active ='2')
-      AND alias_domain.active='1';
-EOF
-
-# MX based routing
-cat <<EOF > /opt/postfix/conf/sql/mysql_mbr_access_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT CONCAT('FILTER smtp_via_transport_maps:', nexthop) as transport FROM transports
-  WHERE '%s' REGEXP destination
-    AND active='1'
-    AND is_mx_based='1';
-EOF
-
-cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf
-# Autogenerated by mailcow
-user = ${DBUSER}
-password = ${DBPASS}
-hosts = unix:/var/run/mysqld/mysqld.sock
-dbname = ${DBNAME}
-query = SELECT goto FROM spamalias
-  WHERE address='%s'
-    AND validity >= UNIX_TIMESTAMP()
-EOF
-
-if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
-  cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
-# This file can be edited.
-# Delete this file and restart postfix container to revert any changes.
-postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
-  hostkarma.junkemailfilter.com=127.0.0.1*-2
-  list.dnswl.org=127.0.[0..255].0*-2
-  list.dnswl.org=127.0.[0..255].1*-4
-  list.dnswl.org=127.0.[0..255].2*-6
-  list.dnswl.org=127.0.[0..255].3*-8
-  bl.spamcop.net*2
-  bl.suomispam.net*2
-  hostkarma.junkemailfilter.com=127.0.0.2*3
-  hostkarma.junkemailfilter.com=127.0.0.4*2
-  hostkarma.junkemailfilter.com=127.0.1.2*1
-  backscatter.spameatingmonkey.net*2
-  bl.ipv6.spameatingmonkey.net*2
-  bl.spameatingmonkey.net*2
-  b.barracudacentral.org=127.0.0.2*7
-  bl.mailspike.net=127.0.0.2*5
-  bl.mailspike.net=127.0.0.[10;11;12]*4
-EOF
-fi
-
-# Remove discontinued DNSBLs from existing dns_blocklists.cf
-sed -i '/ix\.dnsbl\.manitu\.net\*2/d' /opt/postfix/conf/dns_blocklists.cf # Nixspam
-
-DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
-
-if [ ! -z "$DNSBL_CONFIG" ]; then
-  echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
-  if [ -n "$SPAMHAUS_DQS_KEY" ]; then
-    echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
-    echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
-    SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
-  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
-  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
-  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
-  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
-postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
-EOF
-
-  cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
-# Autogenerated by mailcow, using Spamhaus DQS reply domains
-${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net     sbl.spamhaus.org
-${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net     xbl.spamhaus.org
-${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net     pbl.spamhaus.org
-${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net     zen.spamhaus.org
-${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net     dbl.spamhaus.org
-${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net     zrd.spamhaus.org
-EOF
-    )
-  else
-    if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
-      rm /opt/postfix/conf/dnsbl_reply.map
-    fi
-    response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
-    if [ "$response" -eq 503 ]; then
-      echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
-      echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
-      SPAMHAUS_DNSBL_CONFIG=""
-    elif [ "$response" -eq 200 ]; then
-      echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
-      echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
-      SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
-  zen.spamhaus.org=127.0.0.[10;11]*8
-  zen.spamhaus.org=127.0.0.[4..7]*6
-  zen.spamhaus.org=127.0.0.3*4
-  zen.spamhaus.org=127.0.0.2*3
-EOF
-      )
-
-    else
-      echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
-      echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
-      SPAMHAUS_DNSBL_CONFIG=""
-    fi
-  fi
-fi
-
-# Reset main.cf
-sed -i '/Overrides/q' /opt/postfix/conf/main.cf
-echo >> /opt/postfix/conf/main.cf
-# Append postscreen dnsbl sites to main.cf
-if [ ! -z "$DNSBL_CONFIG" ]; then
-  echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
-fi
-# Append user overrides
-echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
-touch /opt/postfix/conf/extra.cf
-sed -i '/\$myhostname/! { /myhostname/d }' /opt/postfix/conf/extra.cf
-echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
-cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
-
-if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
-  echo "Creating dummy custom_transport.pcre"
-  touch /opt/postfix/conf/custom_transport.pcre
-fi
-
-if [[ ! -f /opt/postfix/conf/custom_postscreen_whitelist.cidr ]]; then
-  echo "Creating dummy custom_postscreen_whitelist.cidr"
-  cat <<EOF > /opt/postfix/conf/custom_postscreen_whitelist.cidr
-# Autogenerated by mailcow
-# Rules are evaluated in the order as specified.
-# Blacklist 192.168.* except 192.168.0.1.
-# 192.168.0.1          permit
-# 192.168.0.0/16       reject
-EOF
-fi
-
-# Fix Postfix permissions
-chown -R root:postfix /opt/postfix/conf/sql/ /opt/postfix/conf/custom_transport.pcre
-chmod 640 /opt/postfix/conf/sql/*.cf /opt/postfix/conf/custom_transport.pcre
-chgrp -R postdrop /var/spool/postfix/public
-chgrp -R postdrop /var/spool/postfix/maildrop
-postfix set-permissions
-
-# Checking if there is a leftover of a crashed postfix container before starting a new one
-if [ -e /var/spool/postfix/pid/master.pid ]; then
-  rm -rf /var/spool/postfix/pid/master.pid
-fi
-
-# Check Postfix configuration
-postconf -c /opt/postfix/conf > /dev/null
-
-if [[ $? != 0 ]]; then
-  echo "Postfix configuration error, refusing to start."
-  exit 1
-else
-  postfix -c /opt/postfix/conf start
-  sleep 126144000
-fi

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

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

+ 1 - 1
data/Dockerfiles/sogo/docker-entrypoint.sh

@@ -8,7 +8,7 @@ for file in /hooks/*; do
   fi
 done
 
-python3 /bootstrap/main.py
+python3 -u /bootstrap/main.py
 BOOTSTRAP_EXIT_CODE=$?
 
 if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then

+ 0 - 0
data/conf/nginx/templates/listen_plain.active.j2 → data/conf/nginx/config_templates/listen_plain.active.j2


+ 0 - 0
data/conf/nginx/templates/listen_ssl.active.j2 → data/conf/nginx/config_templates/listen_ssl.active.j2


+ 0 - 0
data/conf/nginx/templates/nginx.conf.j2 → data/conf/nginx/config_templates/nginx.conf.j2


+ 0 - 0
data/conf/nginx/templates/server_name.active.j2 → data/conf/nginx/config_templates/server_name.active.j2


+ 0 - 0
data/conf/nginx/templates/sites-default.conf.j2 → data/conf/nginx/config_templates/sites-default.conf.j2


+ 4 - 0
data/conf/postfix/config_templates/aliases.j2

@@ -0,0 +1,4 @@
+null: /dev/null
+watchdog: /dev/null
+ham: "|/usr/local/bin/rspamd-pipe-ham"
+spam: "|/usr/local/bin/rspamd-pipe-spam"

+ 4 - 0
data/conf/postfix/config_templates/custom_postscreen_whitelist.cidr.j2

@@ -0,0 +1,4 @@
+# Rules are evaluated in the order as specified.
+# Blacklist 192.168.* except 192.168.0.1.
+# 192.168.0.1          permit
+# 192.168.0.0/16       reject

+ 0 - 0
data/conf/postfix/config_templates/custom_transport.pcre.j2


+ 35 - 0
data/conf/postfix/config_templates/dns_blocklists.cf.j2

@@ -0,0 +1,35 @@
+postscreen_dnsbl_sites =
+  wl.mailspike.net=127.0.0.[18;19;20]*-2
+  hostkarma.junkemailfilter.com=127.0.0.1*-2
+  list.dnswl.org=127.0.[0..255].0*-2
+  list.dnswl.org=127.0.[0..255].1*-4
+  list.dnswl.org=127.0.[0..255].2*-6
+  list.dnswl.org=127.0.[0..255].3*-8
+  bl.spamcop.net*2
+  bl.suomispam.net*2
+  hostkarma.junkemailfilter.com=127.0.0.2*3
+  hostkarma.junkemailfilter.com=127.0.0.4*2
+  hostkarma.junkemailfilter.com=127.0.1.2*1
+  backscatter.spameatingmonkey.net*2
+  bl.ipv6.spameatingmonkey.net*2
+  bl.spameatingmonkey.net*2
+  b.barracudacentral.org=127.0.0.2*7
+  bl.mailspike.net=127.0.0.2*5
+  bl.mailspike.net=127.0.0.[10;11;12]*4
+{% if not SKIP_SPAMHAUS %}
+{% if not SPAMHAUS_DQS_KEY %}
+  zen.spamhaus.org=127.0.0.[10;11]*8
+  zen.spamhaus.org=127.0.0.[4..7]*6
+  zen.spamhaus.org=127.0.0.3*4
+  zen.spamhaus.org=127.0.0.2*3
+{% else %}
+  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
+  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
+  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
+  ${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
+{% endif %}
+{% endif %}
+
+{% if SPAMHAUS_DQS_KEY %}
+postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
+{% endif %}

+ 8 - 0
data/conf/postfix/config_templates/dns_reply.map.j2

@@ -0,0 +1,8 @@
+{% if SPAMHAUS_DQS_KEY %}
+${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net     sbl.spamhaus.org
+${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net     xbl.spamhaus.org
+${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net     pbl.spamhaus.org
+${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net     zen.spamhaus.org
+${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net     dbl.spamhaus.org
+${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net     zrd.spamhaus.org
+{% endif %}

+ 179 - 0
data/conf/postfix/config_templates/main.cf.j2

@@ -0,0 +1,179 @@
+# --------------------------------------------------------------------------
+# Please create a file "extra.cf" for persistent overrides to main.cf
+# --------------------------------------------------------------------------
+biff = no
+append_dot_mydomain = no
+smtpd_tls_cert_file = /etc/ssl/mail/cert.pem
+smtpd_tls_key_file = /etc/ssl/mail/key.pem
+tls_server_sni_maps = hash:/opt/postfix/conf/sni.map
+smtpd_tls_received_header = yes
+smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
+smtpd_relay_restrictions = permit_mynetworks,
+  permit_sasl_authenticated,
+  defer_unauth_destination
+smtpd_forbid_bare_newline = yes
+# alias maps are auto-generated in postfix.sh on startup
+alias_maps = hash:/etc/aliases
+alias_database = hash:/etc/aliases
+relayhost =
+mynetworks_style = subnet
+mailbox_size_limit = 0
+recipient_delimiter = +
+inet_interfaces = all
+inet_protocols = all
+bounce_queue_lifetime = 1d
+broken_sasl_auth_clients = yes
+disable_vrfy_command = yes
+maximal_backoff_time = 1800s
+maximal_queue_lifetime = 5d
+delay_warning_time = 4h
+message_size_limit = 104857600
+milter_default_action = tempfail
+milter_protocol = 6
+minimal_backoff_time = 300s
+plaintext_reject_code = 550
+postscreen_access_list = permit_mynetworks,
+  cidr:/opt/postfix/conf/custom_postscreen_whitelist.cidr,
+  cidr:/opt/postfix/conf/postscreen_access.cidr,
+  tcp:127.0.0.1:10027
+postscreen_bare_newline_enable = no
+postscreen_blacklist_action = drop
+postscreen_cache_cleanup_interval = 24h
+postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
+postscreen_dnsbl_action = enforce
+postscreen_dnsbl_threshold = 6
+postscreen_dnsbl_ttl = 5m
+postscreen_greet_action = enforce
+postscreen_greet_banner = $smtpd_banner
+postscreen_greet_ttl = 2d
+postscreen_greet_wait = 3s
+postscreen_non_smtp_command_enable = no
+postscreen_pipelining_enable = no
+proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
+  $sender_dependent_default_transport_maps,
+  $smtp_tls_policy_maps,
+  $local_recipient_maps,
+  $mydestination,
+  $virtual_alias_maps,
+  $virtual_alias_domains,
+  $virtual_mailbox_maps,
+  $virtual_mailbox_domains,
+  $relay_recipient_maps,
+  $relay_domains,
+  $canonical_maps,
+  $sender_canonical_maps,
+  $sender_bcc_maps,
+  $recipient_bcc_maps,
+  $recipient_canonical_maps,
+  $relocated_maps,
+  $transport_maps,
+  $mynetworks,
+  $smtpd_sender_login_maps,
+  $smtp_sasl_password_maps
+queue_run_delay = 300s
+relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
+relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
+sender_dependent_default_transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
+smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
+smtp_tls_cert_file = /etc/ssl/mail/cert.pem
+smtp_tls_key_file = /etc/ssl/mail/key.pem
+smtp_tls_loglevel = 1
+smtp_dns_support_level = dnssec
+smtp_tls_security_level = dane
+smtpd_data_restrictions = reject_unauth_pipelining, permit
+smtpd_delay_reject = yes
+smtpd_error_sleep_time = 10s
+smtpd_forbid_bare_newline = yes
+smtpd_hard_error_limit = ${stress?1}${stress:5}
+smtpd_helo_required = yes
+smtpd_proxy_timeout = 600s
+smtpd_recipient_restrictions = check_recipient_mx_access proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
+  permit_sasl_authenticated,
+  permit_mynetworks,
+  check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
+  reject_invalid_helo_hostname,
+  reject_unauth_destination
+smtpd_sasl_auth_enable = yes
+smtpd_sasl_authenticated_header = yes
+smtpd_sasl_path = inet:dovecot:10001
+smtpd_sasl_type = dovecot
+smtpd_sender_login_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
+smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
+  permit_mynetworks,
+  permit_sasl_authenticated,
+  reject_unlisted_sender,
+  reject_unknown_sender_domain
+smtpd_soft_error_limit = 3
+smtpd_tls_auth_only = yes
+smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
+smtpd_tls_eecdh_grade = auto
+smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
+smtpd_tls_loglevel = 1
+
+# Mandatory protocols and ciphers are used when a connections is enforced to use TLS
+# Does _not_ apply to enforced incoming TLS settings per mailbox
+smtp_tls_mandatory_protocols = >=TLSv1.2
+lmtp_tls_mandatory_protocols = >=TLSv1.2
+smtpd_tls_mandatory_protocols = >=TLSv1.2
+smtpd_tls_mandatory_ciphers = high
+
+smtp_tls_protocols = >=TLSv1.2
+lmtp_tls_protocols = >=TLSv1.2
+smtpd_tls_protocols = >=TLSv1.2
+
+smtpd_tls_security_level = may
+tls_preempt_cipherlist = yes
+tls_ssl_options = NO_COMPRESSION, NO_RENEGOTIATION
+virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
+virtual_gid_maps = static:5000
+virtual_mailbox_base = /var/vmail/
+virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
+# -- moved to rspamd on 2021-06-01
+#recipient_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
+#sender_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
+recipient_canonical_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
+recipient_canonical_classes = envelope_recipient
+virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
+virtual_minimum_uid = 104
+virtual_transport = lmtp:inet:dovecot:24
+virtual_uid_maps = static:5000
+smtpd_milters = inet:rspamd:9900
+non_smtpd_milters = inet:rspamd:9900
+milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
+mydestination = localhost.localdomain, localhost
+smtp_address_preference = any
+smtp_sender_dependent_authentication = yes
+smtp_sasl_auth_enable = yes
+smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
+smtp_sasl_security_options =
+smtp_sasl_mechanism_filter = plain, login
+smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
+smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
+mail_name = Postcow
+# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
+# Use custom_transport.pcre for custom transports
+transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
+  pcre:/opt/postfix/conf/local_transport,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
+  proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
+smtp_sasl_auth_soft_bounce = no
+postscreen_discard_ehlo_keywords = chunking, silent-discard, smtputf8, dsn
+smtpd_discard_ehlo_keywords = chunking, silent-discard, smtputf8
+compatibility_level = 3.7
+# Define protocols for SMTPS and submission service
+submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
+smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2
+parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
+# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot)
+lmtp_destination_recipient_limit=1
+
+{% include "dns_blocklists.cf.j2" %}
+
+myhostname = {{ MAILCOW_HOSTNAME }}
+
+{{ EXTRA_CF | safe }}

+ 8 - 0
data/conf/postfix/config_templates/mysql_mbr_access_maps.cf.j2

@@ -0,0 +1,8 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT CONCAT('FILTER smtp_via_transport_maps:', nexthop) as transport FROM transports
+  WHERE '%s' REGEXP destination
+    AND active='1'
+    AND is_mx_based='1';

+ 8 - 0
data/conf/postfix/config_templates/mysql_recipient_bcc_maps.cf.j2

@@ -0,0 +1,8 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT bcc_dest FROM bcc_maps
+  WHERE local_dest='%s'
+    AND type='rcpt'
+    AND active='1';

+ 7 - 0
data/conf/postfix/config_templates/mysql_recipient_canonical_maps.cf.j2

@@ -0,0 +1,7 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT new_dest FROM recipient_maps
+  WHERE old_dest='%s'
+    AND active='1';

+ 13 - 0
data/conf/postfix/config_templates/mysql_relay_ne.cf.j2

@@ -0,0 +1,13 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT IF(EXISTS(SELECT address, domain FROM alias
+      WHERE address = '%s'
+        AND domain IN (
+          SELECT domain FROM domain
+            WHERE backupmx = '1'
+              AND relay_all_recipients = '1'
+              AND relay_unknown_only = '1')
+
+      ), 'lmtp:inet:dovecot:24', NULL) AS 'transport'

+ 15 - 0
data/conf/postfix/config_templates/mysql_relay_recipient_maps.cf.j2

@@ -0,0 +1,15 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT DISTINCT
+  CASE WHEN '%d' IN (
+    SELECT domain FROM domain
+      WHERE relay_all_recipients=1
+        AND domain='%d'
+        AND backupmx=1
+  )
+  THEN '%s' ELSE (
+    SELECT goto FROM alias WHERE address='%s' AND active='1'
+  )
+  END AS result;

+ 34 - 0
data/conf/postfix/config_templates/mysql_sasl_passwd_maps_sender_dependent.cf.j2

@@ -0,0 +1,34 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
+  WHERE id IN (
+    SELECT COALESCE(
+      (SELECT id FROM relayhosts
+      LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
+      WHERE relayhosts.active = '1'
+        AND (domain.domain = '%d'
+          OR domain.domain IN (
+            SELECT target_domain FROM alias_domain
+            WHERE alias_domain = '%d'
+          )
+        )
+      ),
+      (SELECT id FROM relayhosts
+      LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
+      WHERE relayhosts.active = '1'
+        AND (
+          mailbox.username IN (
+            SELECT alias.goto from alias
+              JOIN mailbox ON mailbox.username = alias.goto
+                WHERE alias.active = '1'
+                  AND alias.address = '%s'
+                  AND alias.address NOT LIKE '@%%'
+          )
+        )
+      )
+    )
+  )
+  AND active = '1'
+  AND username != '';

+ 9 - 0
data/conf/postfix/config_templates/mysql_sasl_passwd_maps_transport_maps.cf.j2

@@ -0,0 +1,9 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports
+  WHERE nexthop = '%s'
+  AND active = '1'
+  AND username != ''
+  LIMIT 1;

+ 8 - 0
data/conf/postfix/config_templates/mysql_sender_bcc_maps.cf.j2

@@ -0,0 +1,8 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT bcc_dest FROM bcc_maps
+  WHERE local_dest='%s'
+    AND type='sender'
+    AND active='1';

+ 43 - 0
data/conf/postfix/config_templates/mysql_sender_dependent_default_transport_maps.cf.j2

@@ -0,0 +1,43 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps
+  FROM (
+    SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias
+      LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto
+        WHERE (address = '%s'
+          OR address IN (
+            SELECT CONCAT('%u', '@', target_domain) FROM alias_domain
+              WHERE alias_domain = '%d'
+          )
+        )
+        AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_out')) = '1'
+        AND mailbox.active = '1'
+    ), 'smtp_enforced_tls:', 'smtp:') AS 'transport'
+    UNION ALL
+    SELECT COALESCE(
+      (SELECT hostname FROM relayhosts
+      LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
+        WHERE relayhosts.active = '1'
+          AND (
+            mailbox.username IN (SELECT alias.goto from alias
+              JOIN mailbox ON mailbox.username = alias.goto
+                WHERE alias.active = '1'
+                  AND alias.address = '%s'
+                  AND alias.address NOT LIKE '@%%'
+            )
+          )
+      ),
+      (SELECT hostname FROM relayhosts
+      LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
+        WHERE relayhosts.active = '1'
+          AND (domain.domain = '%d'
+            OR domain.domain IN (
+              SELECT target_domain FROM alias_domain
+                WHERE alias_domain = '%d'
+            )
+          )
+      )
+    )
+  ) AS transport_view;

+ 14 - 0
data/conf/postfix/config_templates/mysql_tls_enforce_in_policy.cf.j2

@@ -0,0 +1,14 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT IF(EXISTS(
+  SELECT 'TLS_ACTIVE' FROM alias
+    LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto
+      WHERE (address='%s'
+        OR address IN (
+          SELECT CONCAT('%u', '@', target_domain) FROM alias_domain
+            WHERE alias_domain='%d'
+        )
+      ) AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_in')) = '1' AND mailbox.active = '1'
+  ), 'reject_plaintext_session', NULL) AS 'tls_enforce_in';

+ 5 - 0
data/conf/postfix/config_templates/mysql_tls_policy_override_maps.cf.j2

@@ -0,0 +1,5 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_override WHERE active = '1' AND dest = '%s'

+ 7 - 0
data/conf/postfix/config_templates/mysql_transport_maps.cf.j2

@@ -0,0 +1,7 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT CONCAT('smtp_via_transport_maps:', nexthop) AS transport FROM transports
+  WHERE active = '1'
+  AND destination = '%s';

+ 9 - 0
data/conf/postfix/config_templates/mysql_virtual_alias_domain_maps.cf.j2

@@ -0,0 +1,9 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT username FROM mailbox, alias_domain
+  WHERE alias_domain.alias_domain = '%d'
+    AND mailbox.username = CONCAT('%u', '@', alias_domain.target_domain)
+    AND (mailbox.active = '1' OR mailbox.active = '2')
+    AND alias_domain.active='1'

+ 7 - 0
data/conf/postfix/config_templates/mysql_virtual_alias_maps.cf.j2

@@ -0,0 +1,7 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT goto FROM alias
+  WHERE address='%s'
+    AND (active='1' OR active='2');

+ 10 - 0
data/conf/postfix/config_templates/mysql_virtual_domains_maps.cf.j2

@@ -0,0 +1,10 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1'
+  UNION
+  SELECT domain FROM domain
+    WHERE domain='%s'
+      AND active = '1'
+      AND backupmx = '0'

+ 5 - 0
data/conf/postfix/config_templates/mysql_virtual_mailbox_maps.cf.j2

@@ -0,0 +1,5 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%u/') FROM mailbox WHERE username='%s' AND (active = '1' OR active = '2')

+ 5 - 0
data/conf/postfix/config_templates/mysql_virtual_relay_domain_maps.cf.j2

@@ -0,0 +1,5 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1'

+ 6 - 0
data/conf/postfix/config_templates/mysql_virtual_resource_maps.cf.j2

@@ -0,0 +1,6 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT 'null@localhost' FROM mailbox
+  WHERE kind REGEXP 'location|thing|group' AND username = '%s';

+ 50 - 0
data/conf/postfix/config_templates/mysql_virtual_sender_acl.cf.j2

@@ -0,0 +1,50 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+# First select queries domain and alias_domain to determine if domains are active.
+query = SELECT goto FROM alias
+  WHERE id IN (
+      SELECT COALESCE (
+        (
+          SELECT id FROM alias
+            WHERE address='%s'
+            AND (active='1' OR active='2')
+        ), (
+          SELECT id FROM alias
+            WHERE address='@%d'
+            AND (active='1' OR active='2')
+        )
+      )
+    )
+    AND active='1'
+    AND (domain IN
+      (SELECT domain FROM domain
+        WHERE domain='%d'
+          AND active='1')
+      OR domain in (
+        SELECT alias_domain FROM alias_domain
+          WHERE alias_domain='%d'
+            AND active='1'
+      )
+    )
+  UNION
+  SELECT logged_in_as FROM sender_acl
+    WHERE send_as='@%d'
+      OR send_as='%s'
+      OR send_as='*'
+      OR send_as IN (
+        SELECT CONCAT('@',target_domain) FROM alias_domain
+          WHERE alias_domain = '%d')
+      OR send_as IN (
+        SELECT CONCAT('%u','@',target_domain) FROM alias_domain
+          WHERE alias_domain = '%d')
+      AND logged_in_as NOT IN (
+        SELECT goto FROM alias
+          WHERE address='%s')
+  UNION
+  SELECT username FROM mailbox, alias_domain
+    WHERE alias_domain.alias_domain = '%d'
+      AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain)
+      AND (mailbox.active = '1' OR mailbox.active ='2')
+      AND alias_domain.active='1';

+ 7 - 0
data/conf/postfix/config_templates/mysql_virtual_spamalias_maps.cf.j2

@@ -0,0 +1,7 @@
+user = {{ DBUSER }}
+password = {{ DBPASS }}
+hosts = unix:/var/run/mysqld/mysqld.sock
+dbname = {{ DBNAME }}
+query = SELECT goto FROM spamalias
+  WHERE address='%s'
+    AND validity >= UNIX_TIMESTAMP()

+ 7 - 0
data/conf/postfix/config_templates/sni.map.j2

@@ -0,0 +1,7 @@
+{% if not SKIP_LETS_ENCRYPT|lower in ['y', 'yes'] %}
+{% for cert_dir, domains in VALID_CERT_DIRS.items() %}
+{% for domain in domains %}
+{{ domain }} {{ cert_dir }}/key.pem {{ cert_dir }}/cert.pem
+{% endfor %}
+{% endfor %}
+{% endif %}

+ 0 - 0
data/conf/postfix/dns_reply.map


+ 3 - 2
docker-compose.yml

@@ -200,7 +200,7 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: ghcr.io/mailcow/sogo:nightly-16052025
+      image: ghcr.io/mailcow/sogo:nightly-19052025
       environment:
         - CONTAINER_NAME=sogo-mailcow
         - DBNAME=${DBNAME}
@@ -339,7 +339,7 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: ghcr.io/mailcow/postfix:1.80
+      image: ghcr.io/mailcow/postfix:nightly-19052025
       depends_on:
         mysql-mailcow:
           condition: service_started
@@ -354,6 +354,7 @@ services:
         - rspamd-vol-1:/var/lib/rspamd
         - mysql-socket-vol-1:/var/run/mysqld/
       environment:
+        - CONTAINER_NAME=postfix-mailcow
         - LOG_LINES=${LOG_LINES:-9999}
         - TZ=${TZ}
         - DBNAME=${DBNAME}