diff --git a/data/Dockerfiles/bootstrap/main.py b/data/Dockerfiles/bootstrap/main.py index 50d2bc011..e63bc69ff 100644 --- a/data/Dockerfiles/bootstrap/main.py +++ b/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) diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py b/data/Dockerfiles/bootstrap/modules/BootstrapBase.py index 6192e5505..325bc5c04 100644 --- a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py +++ b/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 \ No newline at end of file diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapNginx.py b/data/Dockerfiles/bootstrap/modules/BootstrapNginx.py index 9733ff422..dd17ab893 100644 --- a/data/Dockerfiles/bootstrap/modules/BootstrapNginx.py +++ b/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(), diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py b/data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py new file mode 100644 index 000000000..c7392e74a --- /dev/null +++ b/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 \ No newline at end of file diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapSogo.py b/data/Dockerfiles/bootstrap/modules/BootstrapSogo.py index 9ce5c8f12..7027c4cde 100644 --- a/data/Dockerfiles/bootstrap/modules/BootstrapSogo.py +++ b/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(), diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 5449360b3..bf2d923ee 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/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 pip install --break-system-packages \ + mysql-connector-python \ + jinja2 -RUN chmod +x /opt/postfix.sh \ - /usr/local/bin/rspamd-pipe-ham \ +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"] diff --git a/data/Dockerfiles/postfix/docker-entrypoint.sh b/data/Dockerfiles/postfix/docker-entrypoint.sh index 7b6c5d4aa..7c188459b 100755 --- a/data/Dockerfiles/postfix/docker-entrypoint.sh +++ b/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 diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh deleted file mode 100755 index e5dbf88fc..000000000 --- a/data/Dockerfiles/postfix/postfix.sh +++ /dev/null @@ -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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 < /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 <> /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 < /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 diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index ba70f8edf..c9936b820 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/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 diff --git a/data/Dockerfiles/sogo/docker-entrypoint.sh b/data/Dockerfiles/sogo/docker-entrypoint.sh index 73831395b..c7c18165b 100755 --- a/data/Dockerfiles/sogo/docker-entrypoint.sh +++ b/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 diff --git a/data/conf/nginx/templates/listen_plain.active.j2 b/data/conf/nginx/config_templates/listen_plain.active.j2 similarity index 100% rename from data/conf/nginx/templates/listen_plain.active.j2 rename to data/conf/nginx/config_templates/listen_plain.active.j2 diff --git a/data/conf/nginx/templates/listen_ssl.active.j2 b/data/conf/nginx/config_templates/listen_ssl.active.j2 similarity index 100% rename from data/conf/nginx/templates/listen_ssl.active.j2 rename to data/conf/nginx/config_templates/listen_ssl.active.j2 diff --git a/data/conf/nginx/templates/nginx.conf.j2 b/data/conf/nginx/config_templates/nginx.conf.j2 similarity index 100% rename from data/conf/nginx/templates/nginx.conf.j2 rename to data/conf/nginx/config_templates/nginx.conf.j2 diff --git a/data/conf/nginx/templates/server_name.active.j2 b/data/conf/nginx/config_templates/server_name.active.j2 similarity index 100% rename from data/conf/nginx/templates/server_name.active.j2 rename to data/conf/nginx/config_templates/server_name.active.j2 diff --git a/data/conf/nginx/templates/sites-default.conf.j2 b/data/conf/nginx/config_templates/sites-default.conf.j2 similarity index 100% rename from data/conf/nginx/templates/sites-default.conf.j2 rename to data/conf/nginx/config_templates/sites-default.conf.j2 diff --git a/data/conf/postfix/config_templates/aliases.j2 b/data/conf/postfix/config_templates/aliases.j2 new file mode 100644 index 000000000..16b2a93f6 --- /dev/null +++ b/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" diff --git a/data/conf/postfix/config_templates/custom_postscreen_whitelist.cidr.j2 b/data/conf/postfix/config_templates/custom_postscreen_whitelist.cidr.j2 new file mode 100644 index 000000000..e8130811c --- /dev/null +++ b/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 diff --git a/data/conf/postfix/config_templates/custom_transport.pcre.j2 b/data/conf/postfix/config_templates/custom_transport.pcre.j2 new file mode 100644 index 000000000..e69de29bb diff --git a/data/conf/postfix/config_templates/dns_blocklists.cf.j2 b/data/conf/postfix/config_templates/dns_blocklists.cf.j2 new file mode 100644 index 000000000..8ebda9351 --- /dev/null +++ b/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 %} \ No newline at end of file diff --git a/data/conf/postfix/config_templates/dns_reply.map.j2 b/data/conf/postfix/config_templates/dns_reply.map.j2 new file mode 100644 index 000000000..29521e869 --- /dev/null +++ b/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 %} diff --git a/data/conf/postfix/config_templates/main.cf.j2 b/data/conf/postfix/config_templates/main.cf.j2 new file mode 100644 index 000000000..25cf1a9d3 --- /dev/null +++ b/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 }} diff --git a/data/conf/postfix/config_templates/mysql_mbr_access_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_mbr_access_maps.cf.j2 new file mode 100644 index 000000000..546c2a6c1 --- /dev/null +++ b/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'; diff --git a/data/conf/postfix/config_templates/mysql_recipient_bcc_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_recipient_bcc_maps.cf.j2 new file mode 100644 index 000000000..4893f80f0 --- /dev/null +++ b/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'; diff --git a/data/conf/postfix/config_templates/mysql_recipient_canonical_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_recipient_canonical_maps.cf.j2 new file mode 100644 index 000000000..c33cc2823 --- /dev/null +++ b/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'; diff --git a/data/conf/postfix/config_templates/mysql_relay_ne.cf.j2 b/data/conf/postfix/config_templates/mysql_relay_ne.cf.j2 new file mode 100644 index 000000000..763fa1378 --- /dev/null +++ b/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' diff --git a/data/conf/postfix/config_templates/mysql_relay_recipient_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_relay_recipient_maps.cf.j2 new file mode 100644 index 000000000..81d29db0d --- /dev/null +++ b/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; \ No newline at end of file diff --git a/data/conf/postfix/config_templates/mysql_sasl_passwd_maps_sender_dependent.cf.j2 b/data/conf/postfix/config_templates/mysql_sasl_passwd_maps_sender_dependent.cf.j2 new file mode 100644 index 000000000..d409b33f0 --- /dev/null +++ b/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 != ''; diff --git a/data/conf/postfix/config_templates/mysql_sasl_passwd_maps_transport_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_sasl_passwd_maps_transport_maps.cf.j2 new file mode 100644 index 000000000..c5d442eed --- /dev/null +++ b/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; diff --git a/data/conf/postfix/config_templates/mysql_sender_bcc_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_sender_bcc_maps.cf.j2 new file mode 100644 index 000000000..f490d1c9e --- /dev/null +++ b/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'; diff --git a/data/conf/postfix/config_templates/mysql_sender_dependent_default_transport_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_sender_dependent_default_transport_maps.cf.j2 new file mode 100644 index 000000000..efedbbde4 --- /dev/null +++ b/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; diff --git a/data/conf/postfix/config_templates/mysql_tls_enforce_in_policy.cf.j2 b/data/conf/postfix/config_templates/mysql_tls_enforce_in_policy.cf.j2 new file mode 100644 index 000000000..0144e17c8 --- /dev/null +++ b/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'; \ No newline at end of file diff --git a/data/conf/postfix/config_templates/mysql_tls_policy_override_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_tls_policy_override_maps.cf.j2 new file mode 100644 index 000000000..c0632bf47 --- /dev/null +++ b/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' diff --git a/data/conf/postfix/config_templates/mysql_transport_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_transport_maps.cf.j2 new file mode 100644 index 000000000..5c3e15043 --- /dev/null +++ b/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'; \ No newline at end of file diff --git a/data/conf/postfix/config_templates/mysql_virtual_alias_domain_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_alias_domain_maps.cf.j2 new file mode 100644 index 000000000..f7699306d --- /dev/null +++ b/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' diff --git a/data/conf/postfix/config_templates/mysql_virtual_alias_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_alias_maps.cf.j2 new file mode 100644 index 000000000..e58755966 --- /dev/null +++ b/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'); diff --git a/data/conf/postfix/config_templates/mysql_virtual_domains_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_domains_maps.cf.j2 new file mode 100644 index 000000000..6b644328d --- /dev/null +++ b/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' diff --git a/data/conf/postfix/config_templates/mysql_virtual_mailbox_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_mailbox_maps.cf.j2 new file mode 100644 index 000000000..fe2c24f5c --- /dev/null +++ b/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') diff --git a/data/conf/postfix/config_templates/mysql_virtual_relay_domain_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_relay_domain_maps.cf.j2 new file mode 100644 index 000000000..ccc45df8d --- /dev/null +++ b/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' diff --git a/data/conf/postfix/config_templates/mysql_virtual_resource_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_resource_maps.cf.j2 new file mode 100644 index 000000000..1be80a73b --- /dev/null +++ b/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'; diff --git a/data/conf/postfix/config_templates/mysql_virtual_sender_acl.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_sender_acl.cf.j2 new file mode 100644 index 000000000..818c3161c --- /dev/null +++ b/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'; diff --git a/data/conf/postfix/config_templates/mysql_virtual_spamalias_maps.cf.j2 b/data/conf/postfix/config_templates/mysql_virtual_spamalias_maps.cf.j2 new file mode 100644 index 000000000..19d160558 --- /dev/null +++ b/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() diff --git a/data/conf/postfix/config_templates/sni.map.j2 b/data/conf/postfix/config_templates/sni.map.j2 new file mode 100644 index 000000000..b3d7d0b25 --- /dev/null +++ b/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 %} diff --git a/data/conf/postfix/dns_reply.map b/data/conf/postfix/dns_reply.map new file mode 100644 index 000000000..e69de29bb diff --git a/docker-compose.yml b/docker-compose.yml index d531083ce..9750beb92 100644 --- a/docker-compose.yml +++ b/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}