From 92aa0bbe3292639602623be424d142272a4e2b4c Mon Sep 17 00:00:00 2001 From: Liam Noonan Date: Thu, 16 Jul 2026 05:30:46 +0000 Subject: [PATCH] [IMP] Improve devel mail testing Align it with the prod pattern of using separate ports for incoming and outgoing --- common.yaml | 26 +++-- devel.yaml | 14 ++- mail-prod-config/user-patches.sh | 46 ++++---- .../docker-mailserver/create-config.sh | 33 ------ .../docker-mailserver/postfix-main.cf | 4 - .../docker-mailserver/user-patches.sh | 102 ++++++++++++++++++ .../roundcube_user_settings.inc.php | 3 +- prod.yaml | 3 - 8 files changed, 159 insertions(+), 72 deletions(-) delete mode 100644 mail-testing-config/docker-mailserver/create-config.sh delete mode 100644 mail-testing-config/docker-mailserver/postfix-main.cf create mode 100755 mail-testing-config/docker-mailserver/user-patches.sh rename mail-testing-config/roundcube/{ => config}/roundcube_user_settings.inc.php (73%) diff --git a/common.yaml b/common.yaml index 141a98a..f5d8633 100644 --- a/common.yaml +++ b/common.yaml @@ -16,6 +16,7 @@ services: tty: true volumes: - filestore:/var/lib/odoo + - mailgate:/opt/odoo/custom/src/odoo/addons/mail/static/scripts:rw,z labels: traefik.backend.buffering.retryExpression: IsNetworkError() && Attempts() < 5 traefik.docker.network: "inverseproxy_shared" @@ -53,29 +54,34 @@ services: ENABLE_POLICYD_SPF: 0 ENABLE_SRS: 0 ENABLE_QUOTAS: 0 + MAILGATE_SMTP_PORT: "13525" ODOO_RECEIVING_DOMAINS: manmanufacturing.com entrypoint: - - bash + - /bin/bash - -c - | - cp -r /tmp/docker-mailserver-staging/ /tmp/docker-mailserver - chmod 755 /tmp/docker-mailserver/create-config.sh - /tmp/docker-mailserver/create-config.sh - exec supervisord -c /etc/supervisor/supervisord.conf + set -euo pipefail + mkdir -p /tmp/docker-mailserver + echo "$${MAILBOX_USER}@$${MAILBOX_DOMAIN}|{PLAIN}$${ADMIN_PASSWORD}" \ + > /tmp/docker-mailserver/postfix-accounts.cf + exec /usr/bin/dumb-init -- supervisord -c /etc/supervisor/supervisord.conf volumes: - - ./mail-testing-config/docker-mailserver/:/tmp/docker-mailserver-staging/:ro - - ./odoo/custom/src/odoo/addons/mail/static/scripts/odoo-mailgate.py:/tmp/odoo-mailgate.py:ro + - ./mail-testing-config/docker-mailserver/user-patches.sh:/tmp/docker-mailserver/user-patches.sh:ro + - mailgate:/tmp/mailgate:ro,z email_client_fake: image: roundcube/roundcubemail environment: + ROUNDCUBEMAIL_DB_TYPE: sqlite ROUNDCUBEMAIL_COMPOSER_PLUGINS: "texxasrulez/persistent_login" ROUNDCUBEMAIL_PLUGINS: "show_additional_headers,persistent_login,autocreate_identity" depends_on: - mailserver volumes: - - ./mail-testing-config/roundcube/roundcube_user_settings.inc.php:/var/roundcube/config/config.inc.php:ro + - ./mail-testing-config/roundcube/config/roundcube_user_settings.inc.php:/var/roundcube/config/config.inc.php:ro - ./mail-testing-config/roundcube/plugins/autocreate_identity:/var/www/html/plugins/autocreate_identity + - roundcube:/var/www/html:rw + - roundcube_db:/var/roundcube/db:rw smtpreal: image: ghcr.io/docker-mailserver/docker-mailserver:latest @@ -87,6 +93,7 @@ services: - maillogs:/var/log/mail - maillogssupervisord:/var/log/supervisor - mailstate:/var/mail-state + - mailgate:/tmp/mailgate:ro,z cap_add: - SYS_PTRACE environment: @@ -122,3 +129,6 @@ services: volumes: - backup_cache:/root - filestore:/mnt/backup/src/odoo + +volumes: + mailgate: diff --git a/devel.yaml b/devel.yaml index 9bf57f8..e641f5a 100644 --- a/devel.yaml +++ b/devel.yaml @@ -100,7 +100,8 @@ services: default: environment: ODOO_DB: *dbname - USER_ID: 2 + ODOO_USER_ID: 2 + MAILGATE_SMTP_PORT: "13525" MAILBOX_DOMAIN: *mailserver MAILBOX_USER: admin ADMIN_PASSWORD: admin @@ -117,9 +118,7 @@ services: ROUNDCUBEMAIL_USERNAME_DOMAIN: *mailserver ROUNDCUBEMAIL_DEFAULT_HOST: *mailserver ROUNDCUBEMAIL_SMTP_SERVER: *mailserver - ROUNDCUBEMAIL_SMTP_PORT: 25 - volumes: - - roundcube:/var/www/html:rw + ROUNDCUBEMAIL_SMTP_PORT: 13525 wdb: image: docker.io/kozea/wdb @@ -182,3 +181,10 @@ volumes: filestore: db: roundcube: + roundcube_db: + mailgate: + driver: local + driver_opts: + type: none + o: bind + device: ${PWD}/odoo/custom/src/odoo/addons/mail/static/scripts diff --git a/mail-prod-config/user-patches.sh b/mail-prod-config/user-patches.sh index 5eeea0c..d843ec7 100644 --- a/mail-prod-config/user-patches.sh +++ b/mail-prod-config/user-patches.sh @@ -1,30 +1,30 @@ echo "Applying user patches..." -# This lets postfix know that it is ok to relay these domains. Otherwise -# it would just reject the incoming mail with "Relay access denied". -postconf -e "relay_domains = $ODOO_RECEIVING_DOMAINS" - # Copy the standard smtp service and make a new one on port 12525 PORT=${MAILGATE_SMTP_PORT:-12525} +# This lets postfix know that it is ok to relay these domains. Otherwise +# it would just reject the incoming mail with "Relay access denied". +postconf -e "relay_domains = ${ODOO_RECEIVING_DOMAINS}" + # Define a custom cleanup service that disables SRS (canonical maps) # If we do not do this SRS rewrites the from emal making a bit of a mess. # ** Note: This is only set on the smtpd-incoming-odoo service, # so outgoing will continue to work with SRS. ** -postconf -Me "cleanup-odoo/unix=cleanup-odoo unix n - n - 0 cleanup" +postconf -Me "cleanup-odoo-incoming/unix=cleanup-odoo-incoming unix n - n - 0 cleanup" postconf -Pe \ - "cleanup-odoo/unix/sender_canonical_maps=" \ - "cleanup-odoo/unix/recipient_canonical_maps=" + "cleanup-odoo-incoming/unix/sender_canonical_maps=" \ + "cleanup-odoo-incoming/unix/recipient_canonical_maps=" -postconf -Me "$PORT/inet=$PORT inet n - n - - smtpd" +postconf -Me "${PORT}/inet=${PORT} inet n - n - - smtpd" # Add configs to this new smtp service postconf -Pe \ - "$PORT/inet/syslog_name=postfix/smtpd-incoming-odoo" \ - "$PORT/inet/cleanup_service_name=cleanup-odoo" \ - "$PORT/inet/smtpd_upstream_proxy_protocol=haproxy" \ - "$PORT/inet/content_filter=odoo_mailgate:dummy" \ - "$PORT/inet/local_recipient_maps=" \ - "$PORT/inet/smtpd_recipient_restrictions=permit_mynetworks,permit_auth_destination,reject" + "${PORT}/inet/syslog_name=postfix/smtpd-incoming-odoo" \ + "${PORT}/inet/cleanup_service_name=cleanup-odoo-incoming" \ + "${PORT}/inet/smtpd_upstream_proxy_protocol=haproxy" \ + "${PORT}/inet/content_filter=odoo_mailgate:dummy" \ + "${PORT}/inet/local_recipient_maps=" \ + "${PORT}/inet/smtpd_recipient_restrictions=permit_mynetworks,permit_auth_destination,reject" postconf -Me "odoo_mailgate/unix=odoo_mailgate unix - n n - - pipe user=nobody argv=/usr/local/bin/odoo-mailgate-wrapper.py" @@ -44,10 +44,10 @@ def log_message(level, message): try: # Configuration from environment variables - DB = "$ODOO_DB" - USER = "$ODOO_USER_ID" - PASSWORD = "$ADMIN_PASSWORD" - SECRET = "$HEADER_SECRET" + DB = "${ODOO_DB}" + USER = "${ODOO_USER_ID}" + PASSWORD = "${ADMIN_PASSWORD}" + SECRET = "${HEADER_SECRET}" if not all([DB, USER, PASSWORD, SECRET]): log_message("err", "Odoo mailgate: Missing one or more required environment variables.") @@ -92,7 +92,15 @@ try: sys.exit(0) # Pipe to odoo-mailgate - cmd = ['/usr/bin/python3', '/usr/local/bin/odoo-mailgate.py', '-d', DB, '-u', USER, '-p', PASSWORD, '--host', 'odoo', '--port', '8069'] + cmd = [ + "/usr/bin/python3", + "/usr/local/bin/odoo-mailgate.py", + "-d", DB, + "-u", USER, + "-p", PASSWORD, + "--host", "odoo", + "--port", "8069", + ] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, text=False) try: proc.stdin.writelines(headers) diff --git a/mail-testing-config/docker-mailserver/create-config.sh b/mail-testing-config/docker-mailserver/create-config.sh deleted file mode 100644 index 2550f4f..0000000 --- a/mail-testing-config/docker-mailserver/create-config.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh - -# Setup postfix/dovecot account -echo "$MAILBOX_USER@$MAILBOX_DOMAIN|{PLAIN}$ADMIN_PASSWORD" >> /tmp/docker-mailserver/postfix-accounts.cf - -# Set up client access to redirect outgoing from odoo (through mailpit) to dovecot -: > /tmp/docker-mailserver/postfix-client-access.cf -echo "/$SMTP_SERVICE_NAME/ REDIRECT $MAILBOX_USER@$MAILBOX_DOMAIN" >> /tmp/docker-mailserver/postfix-client-access.cf - -# Setup postfix transport map -: > /tmp/docker-mailserver/postfix-transport-map.cf -## Mail coming from odoo is delivered to roundcube's mailbox in dovecot -echo "$MAILBOX_USER@$MAILBOX_DOMAIN lmtp:unix:/var/run/dovecot/lmtp" >> /tmp/docker-mailserver/postfix-transport-map.cf -## Mail for the receiving domains is handled by odoo's mailgate without being redirected to an alias -for domain in $(echo "$ODOO_RECEIVING_DOMAINS" | tr ',' ' '); do - echo "$domain odoo_mailgate:" >> /tmp/docker-mailserver/postfix-transport-map.cf -done -## Discard everything else -echo "* discard:" >> /tmp/docker-mailserver/postfix-transport-map.cf - -# Define the 'odoo_mailgate' service in master.cf -# Note that we must do this in /etc/postfix/master.cf directly as docker-mailserver does not support -# defining custom services in /tmp/docker-mailserver/postfix-master.cf -echo "" >> /etc/postfix/master.cf -echo "odoo_mailgate unix - n n - - pipe" >> /etc/postfix/master.cf -echo " user=nobody argv=/usr/local/bin/odoo-mailgate-wrapper.sh" >> /etc/postfix/master.cf - -# Setup Odoo mailgate script -cp /tmp/odoo-mailgate.py /usr/local/bin/odoo-mailgate.py -chmod 755 /usr/local/bin/odoo-mailgate.py -echo '#!/bin/sh' > /usr/local/bin/odoo-mailgate-wrapper.sh -echo "/usr/bin/python3 /usr/local/bin/odoo-mailgate.py -d $ODOO_DB -u $USER_ID -p $ADMIN_PASSWORD --host odoo --port 8069" >> /usr/local/bin/odoo-mailgate-wrapper.sh -chmod 755 /usr/local/bin/odoo-mailgate-wrapper.sh diff --git a/mail-testing-config/docker-mailserver/postfix-main.cf b/mail-testing-config/docker-mailserver/postfix-main.cf deleted file mode 100644 index f5b9978..0000000 --- a/mail-testing-config/docker-mailserver/postfix-main.cf +++ /dev/null @@ -1,4 +0,0 @@ -smtpd_client_restrictions = - check_client_access regexp:/tmp/docker-mailserver/postfix-client-access.cf - -transport_maps = texthash:/tmp/docker-mailserver/postfix-transport-map.cf diff --git a/mail-testing-config/docker-mailserver/user-patches.sh b/mail-testing-config/docker-mailserver/user-patches.sh new file mode 100755 index 0000000..462e0dd --- /dev/null +++ b/mail-testing-config/docker-mailserver/user-patches.sh @@ -0,0 +1,102 @@ +#!/bin/bash +echo "Applying user patches..." + +MAILBOX_USER=${MAILBOX_USER:-admin} +MAILBOX_DOMAIN=${MAILBOX_DOMAIN:-mailserver} +ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin} +SMTP_SERVICE_NAME=${SMTP_SERVICE_NAME:-smtp} +PORT=${MAILGATE_SMTP_PORT:-13525} +POSTFIX_DIR=/etc/postfix + +# Port 25: redirect Mailpit (Odoo outgoing) into the Roundcube mailbox +CLIENT_ACCESS="${POSTFIX_DIR}/odoo-outgoing-client-access.cf" +: > "${CLIENT_ACCESS}" +echo "/${SMTP_SERVICE_NAME}/ REDIRECT ${MAILBOX_USER}@${MAILBOX_DOMAIN}" >> "${CLIENT_ACCESS}" +postconf -e "smtpd_client_restrictions = check_client_access regexp:${CLIENT_ACCESS}" + +# Transport: Odoo domains -> mailgate; mailbox -> Dovecot LMTP; else discard +# (Mailpit traffic is redirected to the mailbox before transport lookup.) +TRANSPORT_MAP="${POSTFIX_DIR}/odoo-transport-map.cf" +: > "${TRANSPORT_MAP}" +echo "${MAILBOX_USER}@${MAILBOX_DOMAIN} lmtp:unix:/var/run/dovecot/lmtp" >> "${TRANSPORT_MAP}" +for domain in $(echo "${ODOO_RECEIVING_DOMAINS}" | tr ',' ' '); do + echo "${domain} odoo_mailgate:" >> "${TRANSPORT_MAP}" +done +echo "* discard:" >> "${TRANSPORT_MAP}" +postconf -e "transport_maps = texthash:${TRANSPORT_MAP}" + +postconf -e "relay_domains = ${ODOO_RECEIVING_DOMAINS}" + +# Port 13525: Incoming to odoo from Roundcube — Odoo domains to mailgate, else to Dovecot +CANONICAL="${POSTFIX_DIR}/odoo-incoming-recipient-canonical.cf" +: > "${CANONICAL}" +# Catch ODOO_RECEIVING_DOMAINS addresses and map them to themselves. +# The rest goes to admin@mailserver +for domain in $(echo "${ODOO_RECEIVING_DOMAINS}" | tr ',' ' '); do + domain_re=$(printf '%s' "${domain}" | sed 's/\./\\./g') + echo "/(.*)@${domain_re}\$/ \$1@${domain}" >> "${CANONICAL}" +done +mailbox_re=$(printf '%s' "${MAILBOX_USER}@${MAILBOX_DOMAIN}" | sed 's/\./\\./g') +echo "/^(${mailbox_re})\$/ \$1" >> "${CANONICAL}" +echo "/.+/ ${MAILBOX_USER}@${MAILBOX_DOMAIN}" >> "${CANONICAL}" + +postconf -Me "cleanup-odoo-incoming/unix=cleanup-odoo-incoming unix n - n - 0 cleanup" +postconf -Pe \ + "cleanup-odoo-incoming/unix/recipient_canonical_maps=regexp:${CANONICAL}" \ + "cleanup-odoo-incoming/unix/recipient_canonical_classes=envelope_recipient" + +postconf -Me "${PORT}/inet=${PORT} inet n - n - - smtpd" +# Add configs to this new smtp service +postconf -Pe \ + "${PORT}/inet/syslog_name=postfix/smtpd-incoming-odoo" \ + "${PORT}/inet/cleanup_service_name=cleanup-odoo-incoming" \ + "${PORT}/inet/local_recipient_maps=" \ + "${PORT}/inet/smtpd_recipient_restrictions=permit_mynetworks,permit_auth_destination,reject" + +postconf -Me "odoo_mailgate/unix=odoo_mailgate unix - n n - - pipe user=nobody argv=/usr/local/bin/odoo-mailgate-wrapper.py" + +# Setup Odoo mailgate script +cp /tmp/mailgate/odoo-mailgate.py /usr/local/bin/odoo-mailgate.py +chmod 755 /usr/local/bin/odoo-mailgate.py +cat < /usr/local/bin/odoo-mailgate-wrapper.py +#!/usr/bin/env python3 +import sys +import subprocess + +def log_message(level, message): + """Logs a message to syslog via the logger command.""" + subprocess.run(['logger', '-t', 'odoo-mailgate', '-p', f'mail.{level}'], input=message.encode()) + +try: + # Configuration from environment variables + DB = "${ODOO_DB}" + USER = "${ODOO_USER_ID}" + PASSWORD = "${ADMIN_PASSWORD}" + + if not all([DB, USER, PASSWORD]): + log_message("err", "Odoo mailgate: Missing one or more required environment variables.") + sys.exit(78) # EX_CONFIG + + # Pipe to odoo-mailgate + cmd = [ + "/usr/bin/python3", + "/usr/local/bin/odoo-mailgate.py", + "-d", DB, + "-u", USER, + "-p", PASSWORD, + "--host", "odoo", + "--port", "8069", + ] + return_code = subprocess.run(cmd, stdin=sys.stdin.buffer).returncode + + if return_code != 0: + log_message("warn", f"Odoo mailgate script failed with exit code {return_code}.") + sys.exit(return_code) + +except Exception as e: + # Fail safe: log the error and exit with a temporary failure code + # so the Mail Transfer Agent retries later. + log_message("err", f"Odoo mailgate wrapper failed with an unexpected error: {e}") + sys.exit(75) # EX_TEMPFAIL +EOF +chmod 755 /usr/local/bin/odoo-mailgate-wrapper.py diff --git a/mail-testing-config/roundcube/roundcube_user_settings.inc.php b/mail-testing-config/roundcube/config/roundcube_user_settings.inc.php similarity index 73% rename from mail-testing-config/roundcube/roundcube_user_settings.inc.php rename to mail-testing-config/roundcube/config/roundcube_user_settings.inc.php index 04f89f8..6f418a3 100644 --- a/mail-testing-config/roundcube/roundcube_user_settings.inc.php +++ b/mail-testing-config/roundcube/config/roundcube_user_settings.inc.php @@ -5,7 +5,8 @@ // Reply above the quote $config['reply_mode'] = 1; -// Turn off SMTP authentication because we are using port 25 +// Turn off SMTP authentication: Roundcube sends on the inbound mailgate +// port (13525), allowed via docker-mailserver mynetworks. // Unfortunately, we cannot set this via environment variables. See: // https://github.com/roundcube/roundcubemail-docker/blob/9fd84b3e4f7943c7b2046d0a443a39d1a702edf2/fpm/docker-entrypoint.sh#L177-#L188 $config['smtp_user'] = ''; diff --git a/prod.yaml b/prod.yaml index 92baf1e..d4693ae 100644 --- a/prod.yaml +++ b/prod.yaml @@ -5,8 +5,6 @@ services: service: odoo restart: unless-stopped hostname: "manmanufacturing.com" - volumes: - - mailgate:/opt/odoo/custom/src/odoo/addons/mail/static/scripts:rw,z env_file: - .docker/odoo.env - .docker/db-access.env @@ -173,7 +171,6 @@ services: - .docker/odoo.env volumes: - inverseproxy_acme:/etc/letsencrypt:rw,z - - mailgate:/tmp/mailgate:ro,z - ./mail-prod-config/user-patches.sh:/tmp/docker-mailserver/user-patches.sh environment: SSL_TYPE: letsencrypt