feature/postfix-pgsql-image #8
38
images/postfix-pgsql/Dockerfile
Normal file
38
images/postfix-pgsql/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Postfix + PostgreSQL Relay/MTA Image
|
||||||
|
#
|
||||||
|
# A pure SMTP relay/MTA: receives inbound mail and forwards it based on
|
||||||
|
# PostgreSQL-backed relay_domains/transport_maps lookups, equivalent to the
|
||||||
|
# Postfix setup described in the SimpleLogin self-hosting documentation
|
||||||
|
# (https://github.com/simple-login/app). No domain, hostname, or database
|
||||||
|
# credentials are baked into the image; everything is templated from
|
||||||
|
# environment variables at container startup. This image has no knowledge
|
||||||
|
# of what consumes the relayed mail.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# POSTFIX_VERSION pins the Debian base image tag (e.g. "12-slim"), which in
|
||||||
|
# turn pins the postfix/postfix-pgsql package versions available via apt.
|
||||||
|
ARG POSTFIX_VERSION=12-slim
|
||||||
|
FROM debian:${POSTFIX_VERSION}
|
||||||
|
|
||||||
|
LABEL maintainer="Damien Arnodo"
|
||||||
|
LABEL description="Postfix SMTP relay/MTA with PostgreSQL-backed relay_domains and transport_maps lookups"
|
||||||
|
|
||||||
|
RUN echo "postfix postfix/main_mailer_type select No configuration" | debconf-set-selections \
|
||||||
|
&& echo "postfix postfix/mailname string localhost" | debconf-set-selections \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
postfix \
|
||||||
|
postfix-pgsql \
|
||||||
|
gettext-base \
|
||||||
|
openssl \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY templates/ /etc/postfix/templates/
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 25
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
1
images/postfix-pgsql/POSTFIX_VERSION
Normal file
1
images/postfix-pgsql/POSTFIX_VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
12-slim
|
||||||
136
images/postfix-pgsql/README.md
Normal file
136
images/postfix-pgsql/README.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Postfix PostgreSQL Relay Image
|
||||||
|
|
||||||
|
A Postfix SMTP relay/MTA whose relay and transport decisions are driven by
|
||||||
|
PostgreSQL lookup maps instead of static config. It receives inbound mail and
|
||||||
|
forwards it to an internal SMTP target after checking the recipient's domain
|
||||||
|
against a database — it has no knowledge of, and makes no assumptions about,
|
||||||
|
what consumes the mail downstream.
|
||||||
|
|
||||||
|
This mirrors the Postfix setup described in the SimpleLogin self-hosting
|
||||||
|
documentation (`postfix` + `postfix-pgsql`, a `relay_domains` PostgreSQL map,
|
||||||
|
and a `transport_maps` PostgreSQL map): https://github.com/simple-login/app
|
||||||
|
|
||||||
|
No domain, hostname, or database credential is hardcoded in the image.
|
||||||
|
Everything is templated from environment variables by
|
||||||
|
`docker-entrypoint.sh` at container startup, then Postfix is started in the
|
||||||
|
foreground (`postfix start-fg`) so it runs correctly under Docker's process
|
||||||
|
supervision.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
On startup, the entrypoint renders three files from templates under
|
||||||
|
`/etc/postfix/templates/`:
|
||||||
|
|
||||||
|
- `/etc/postfix/main.cf` — core Postfix configuration.
|
||||||
|
- `/etc/postfix/pgsql-relay-domains.cf` — PostgreSQL lookup that tells
|
||||||
|
Postfix which domains it should relay mail for.
|
||||||
|
- `/etc/postfix/pgsql-transport-maps.cf` — PostgreSQL lookup that tells
|
||||||
|
Postfix where to forward accepted mail (the internal SMTP relay target).
|
||||||
|
|
||||||
|
Chroot is disabled for all Postfix services via `postconf -F '*/*/chroot = n'`
|
||||||
|
on every startup. Postfix's chrooted child processes (smtpd, trivial-rewrite,
|
||||||
|
etc.) lack access to `/etc/resolv.conf` inside the chroot jail, so they cannot
|
||||||
|
resolve Docker-internal hostnames — causing every PostgreSQL lookup to fail with
|
||||||
|
`451 4.3.0 Temporary lookup failure`. Disabling chroot is the standard remedy
|
||||||
|
in containerised deployments.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
### Required
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|--------------|---------|
|
||||||
|
| `MAIL_DOMAIN` | Mail domain this relay handles (`mydomain`/`myorigin` in `main.cf`). | `example.com` |
|
||||||
|
| `MYHOSTNAME` | Hostname of this relay, used as Postfix's `myhostname` and as the MX target for `MAIL_DOMAIN`. | `mx1.example.com` |
|
||||||
|
| `POSTGRES_HOST` | Hostname of the PostgreSQL server holding the lookup tables. | `postgres` |
|
||||||
|
| `POSTGRES_PORT` | PostgreSQL port. | `5432` |
|
||||||
|
| `POSTGRES_USER` | PostgreSQL user used for the lookup queries. | `postfix` |
|
||||||
|
| `POSTGRES_PASSWORD` | PostgreSQL password for `POSTGRES_USER`. | `change-me` |
|
||||||
|
| `POSTGRES_DB` | PostgreSQL database name. | `relaydb` |
|
||||||
|
| `SMTP_RELAY_HOST` | Internal SMTP host that accepted mail is forwarded to after the relay-domains lookup matches. | `app-internal` |
|
||||||
|
| `SMTP_RELAY_PORT` | Port of the internal SMTP relay target. | `2525` |
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|--------------|
|
||||||
|
| `TLS_CERT_FILE` | Absolute path (inside the container) to a TLS certificate to use for `smtpd_tls_cert_file`. If unset, a self-signed certificate is generated on first start. |
|
||||||
|
| `TLS_KEY_FILE` | Absolute path (inside the container) to the private key matching `TLS_CERT_FILE`. Must be set together with `TLS_CERT_FILE`. |
|
||||||
|
|
||||||
|
The PostgreSQL lookup tables are expected to expose at least a
|
||||||
|
`relay_domain(domain)` table; both lookups query it by recipient domain.
|
||||||
|
|
||||||
|
## TLS certificates
|
||||||
|
|
||||||
|
By default, if `TLS_CERT_FILE`/`TLS_KEY_FILE` are not provided, the
|
||||||
|
entrypoint generates a self-signed certificate at
|
||||||
|
`/etc/postfix/ssl/snakeoil.{pem,key}` on first start (matching the
|
||||||
|
self-signed fallback used in the upstream docs) and reuses it on subsequent
|
||||||
|
restarts if that path is kept on a persistent volume.
|
||||||
|
|
||||||
|
To use your own certificate instead, mount it into the container and point
|
||||||
|
`TLS_CERT_FILE`/`TLS_KEY_FILE` at the mounted paths, e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run \
|
||||||
|
-v /path/to/fullchain.pem:/certs/fullchain.pem:ro \
|
||||||
|
-v /path/to/privkey.pem:/certs/privkey.pem:ro \
|
||||||
|
-e TLS_CERT_FILE=/certs/fullchain.pem \
|
||||||
|
-e TLS_KEY_FILE=/certs/privkey.pem \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### `docker run`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-p 25:25 \
|
||||||
|
-e MAIL_DOMAIN=example.com \
|
||||||
|
-e MYHOSTNAME=mx1.example.com \
|
||||||
|
-e POSTGRES_HOST=postgres \
|
||||||
|
-e POSTGRES_PORT=5432 \
|
||||||
|
-e POSTGRES_USER=postfix \
|
||||||
|
-e POSTGRES_PASSWORD=change-me \
|
||||||
|
-e POSTGRES_DB=relaydb \
|
||||||
|
-e SMTP_RELAY_HOST=app-internal \
|
||||||
|
-e SMTP_RELAY_PORT=2525 \
|
||||||
|
gitea.arnodo.fr/damien/postfix-pgsql:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
postfix:
|
||||||
|
image: gitea.arnodo.fr/damien/postfix-pgsql:latest
|
||||||
|
ports:
|
||||||
|
- "25:25"
|
||||||
|
environment:
|
||||||
|
MAIL_DOMAIN: example.com
|
||||||
|
MYHOSTNAME: mx1.example.com
|
||||||
|
POSTGRES_HOST: postgres
|
||||||
|
POSTGRES_PORT: "5432"
|
||||||
|
POSTGRES_USER: postfix
|
||||||
|
POSTGRES_PASSWORD: change-me
|
||||||
|
POSTGRES_DB: relaydb
|
||||||
|
SMTP_RELAY_HOST: app-internal
|
||||||
|
SMTP_RELAY_PORT: "2525"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version management
|
||||||
|
|
||||||
|
The Debian base image tag is set in `POSTFIX_VERSION` (e.g. `12-slim`),
|
||||||
|
which also pins the `postfix`/`postfix-pgsql` package versions available via
|
||||||
|
`apt` for that Debian release. To change it:
|
||||||
|
|
||||||
|
1. Edit `POSTFIX_VERSION` with the desired Debian tag.
|
||||||
|
2. Commit and push.
|
||||||
|
3. The CI builds the image with that base and tags it with both `latest` and
|
||||||
|
the value of `POSTFIX_VERSION`.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- Upstream Postfix + PostgreSQL setup this image is based on:
|
||||||
|
https://github.com/simple-login/app
|
||||||
68
images/postfix-pgsql/docker-entrypoint.sh
Normal file
68
images/postfix-pgsql/docker-entrypoint.sh
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# Renders Postfix config templates from environment variables and starts
|
||||||
|
# Postfix in the foreground.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TEMPLATE_DIR="/etc/postfix/templates"
|
||||||
|
SSL_DIR="/etc/postfix/ssl"
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
local name="$1"
|
||||||
|
if [ -z "${!name:-}" ]; then
|
||||||
|
echo "ERROR: required environment variable ${name} is not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for var in MAIL_DOMAIN MYHOSTNAME \
|
||||||
|
POSTGRES_HOST POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB \
|
||||||
|
SMTP_RELAY_HOST SMTP_RELAY_PORT; do
|
||||||
|
require_var "$var"
|
||||||
|
done
|
||||||
|
|
||||||
|
# TLS: use mounted cert/key if provided, otherwise generate a self-signed
|
||||||
|
# "snakeoil" certificate on first start, matching the upstream docs' approach.
|
||||||
|
mkdir -p "$SSL_DIR"
|
||||||
|
if [ -n "${TLS_CERT_FILE:-}" ] && [ -n "${TLS_KEY_FILE:-}" ]; then
|
||||||
|
export SMTPD_TLS_CERT_FILE="$TLS_CERT_FILE"
|
||||||
|
export SMTPD_TLS_KEY_FILE="$TLS_KEY_FILE"
|
||||||
|
else
|
||||||
|
export SMTPD_TLS_CERT_FILE="${SSL_DIR}/snakeoil.pem"
|
||||||
|
export SMTPD_TLS_KEY_FILE="${SSL_DIR}/snakeoil.key"
|
||||||
|
if [ ! -f "$SMTPD_TLS_CERT_FILE" ] || [ ! -f "$SMTPD_TLS_KEY_FILE" ]; then
|
||||||
|
echo "No TLS_CERT_FILE/TLS_KEY_FILE provided, generating a self-signed certificate"
|
||||||
|
openssl req -x509 -nodes -days 3650 \
|
||||||
|
-newkey rsa:2048 \
|
||||||
|
-subj "/CN=${MYHOSTNAME}" \
|
||||||
|
-keyout "$SMTPD_TLS_KEY_FILE" \
|
||||||
|
-out "$SMTPD_TLS_CERT_FILE"
|
||||||
|
chmod 600 "$SMTPD_TLS_KEY_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
render() {
|
||||||
|
local template="$1"
|
||||||
|
local destination="$2"
|
||||||
|
envsubst '${MAIL_DOMAIN} ${MYHOSTNAME} ${SMTPD_TLS_CERT_FILE} ${SMTPD_TLS_KEY_FILE} ${POSTGRES_HOST} ${POSTGRES_PORT} ${POSTGRES_USER} ${POSTGRES_PASSWORD} ${POSTGRES_DB} ${SMTP_RELAY_HOST} ${SMTP_RELAY_PORT}' \
|
||||||
|
< "$template" > "$destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
render "${TEMPLATE_DIR}/main.cf.template" /etc/postfix/main.cf
|
||||||
|
render "${TEMPLATE_DIR}/pgsql-relay-domains.cf.template" /etc/postfix/pgsql-relay-domains.cf
|
||||||
|
render "${TEMPLATE_DIR}/pgsql-transport-maps.cf.template" /etc/postfix/pgsql-transport-maps.cf
|
||||||
|
|
||||||
|
# The pgsql lookup config files embed the database password; restrict access.
|
||||||
|
chown root:postfix /etc/postfix/pgsql-relay-domains.cf /etc/postfix/pgsql-transport-maps.cf
|
||||||
|
chmod 640 /etc/postfix/pgsql-relay-domains.cf /etc/postfix/pgsql-transport-maps.cf
|
||||||
|
|
||||||
|
postfix check
|
||||||
|
|
||||||
|
# Disable chroot for all master.cf services so child processes (smtpd,
|
||||||
|
# trivial-rewrite, etc.) can resolve Docker-internal hostnames via the host's
|
||||||
|
# /etc/resolv.conf. Without this, chrooted processes have no DNS and every
|
||||||
|
# pgsql lookup returns "451 4.3.0 Temporary lookup failure".
|
||||||
|
postconf -F '*/*/chroot = n'
|
||||||
|
|
||||||
|
exec /usr/sbin/postfix start-fg
|
||||||
39
images/postfix-pgsql/templates/main.cf.template
Normal file
39
images/postfix-pgsql/templates/main.cf.template
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Postfix main.cf template
|
||||||
|
#
|
||||||
|
# Rendered at container startup by docker-entrypoint.sh, with values
|
||||||
|
# substituted from environment variables. See README.md for the full list
|
||||||
|
# of supported variables.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
myhostname = ${MYHOSTNAME}
|
||||||
|
mydomain = ${MAIL_DOMAIN}
|
||||||
|
myorigin = $mydomain
|
||||||
|
mydestination =
|
||||||
|
inet_interfaces = all
|
||||||
|
inet_protocols = ipv4
|
||||||
|
|
||||||
|
# Dynamic relay/transport decisions backed by PostgreSQL lookup maps.
|
||||||
|
relay_domains = pgsql:/etc/postfix/pgsql-relay-domains.cf
|
||||||
|
transport_maps = pgsql:/etc/postfix/pgsql-transport-maps.cf
|
||||||
|
|
||||||
|
# This instance only relays mail for the domains returned by relay_domains;
|
||||||
|
# it does not accept mail for arbitrary destinations.
|
||||||
|
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination
|
||||||
|
mynetworks = 127.0.0.0/8, [::1]/128
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
smtpd_tls_cert_file = ${SMTPD_TLS_CERT_FILE}
|
||||||
|
smtpd_tls_key_file = ${SMTPD_TLS_KEY_FILE}
|
||||||
|
smtpd_use_tls = yes
|
||||||
|
smtpd_tls_security_level = may
|
||||||
|
smtp_tls_security_level = may
|
||||||
|
|
||||||
|
# Log to stdout/stderr so `docker logs` captures Postfix activity directly,
|
||||||
|
# without needing a syslog daemon inside the container.
|
||||||
|
maillog_file = /dev/stdout
|
||||||
|
|
||||||
|
biff = no
|
||||||
|
append_dot_mydomain = no
|
||||||
|
readme_directory = no
|
||||||
|
compatibility_level = 3.6
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Postfix pgsql_table lookup: relay_domains
|
||||||
|
#
|
||||||
|
# Returns the domain itself when Postfix should relay mail for it.
|
||||||
|
# Rendered at container startup from environment variables.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
user = ${POSTGRES_USER}
|
||||||
|
password = ${POSTGRES_PASSWORD}
|
||||||
|
hosts = ${POSTGRES_HOST}:${POSTGRES_PORT}
|
||||||
|
dbname = ${POSTGRES_DB}
|
||||||
|
query = SELECT domain FROM relay_domain WHERE domain='%s'
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Postfix pgsql_table lookup: transport_maps
|
||||||
|
#
|
||||||
|
# Returns the internal SMTP relay target that accepted mail gets forwarded
|
||||||
|
# to once a recipient domain matches relay_domains. The target itself is
|
||||||
|
# baked into the query from SMTP_RELAY_HOST/SMTP_RELAY_PORT at startup; this
|
||||||
|
# image has no further knowledge of what consumes the mail downstream.
|
||||||
|
# Rendered at container startup from environment variables.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
user = ${POSTGRES_USER}
|
||||||
|
password = ${POSTGRES_PASSWORD}
|
||||||
|
hosts = ${POSTGRES_HOST}:${POSTGRES_PORT}
|
||||||
|
dbname = ${POSTGRES_DB}
|
||||||
|
query = SELECT 'smtp:[${SMTP_RELAY_HOST}]:${SMTP_RELAY_PORT}' FROM relay_domain WHERE domain='%s'
|
||||||
142
images/postfix-pgsql/tests/setup.md
Normal file
142
images/postfix-pgsql/tests/setup.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Local Test Environment Setup
|
||||||
|
|
||||||
|
Step-by-step instructions for standing up a self-contained test environment for
|
||||||
|
the `postfix-pgsql` image on any Linux host with Docker installed. No cloud
|
||||||
|
access is required.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
External SMTP client (e.g. swaks / telnet)
|
||||||
|
|
|
||||||
|
| :25
|
||||||
|
v
|
||||||
|
[ postfix-pgsql ] ──pgsql lookup──> [ postgres ]
|
||||||
|
|
|
||||||
|
| :2525 (SMTP_RELAY_PORT)
|
||||||
|
v
|
||||||
|
[ mailhog ] <── captured mail browsable at :8025
|
||||||
|
```
|
||||||
|
|
||||||
|
MailHog acts as a stand-in for the downstream SMTP consumer and captures every
|
||||||
|
message delivered to it, making it easy to verify end-to-end relay without
|
||||||
|
actually sending real mail.
|
||||||
|
|
||||||
|
## 1. Create a dedicated Docker network
|
||||||
|
|
||||||
|
All containers must share the same network so they can resolve each other by
|
||||||
|
container name (required because chroot is disabled in this image — see the
|
||||||
|
README).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create postfix-test-net
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Start PostgreSQL with the relay_domain schema
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name test-postgres \
|
||||||
|
--network postfix-test-net \
|
||||||
|
-e POSTGRES_USER=relayuser \
|
||||||
|
-e POSTGRES_PASSWORD=testpassword \
|
||||||
|
-e POSTGRES_DB=relaydb \
|
||||||
|
postgres:16-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for Postgres to be ready, then create the `relay_domain` table and insert
|
||||||
|
a test domain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -i test-postgres psql -U relayuser -d relaydb <<'SQL'
|
||||||
|
CREATE TABLE relay_domain (domain TEXT PRIMARY KEY);
|
||||||
|
INSERT INTO relay_domain VALUES ('example.com');
|
||||||
|
SQL
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
```bash
|
||||||
|
docker exec -i test-postgres psql -U relayuser -d relaydb -c "SELECT * FROM relay_domain;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Start MailHog
|
||||||
|
|
||||||
|
MailHog listens on port 2525 for SMTP (matching `SMTP_RELAY_PORT`) and exposes
|
||||||
|
a web UI on port 8025.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name test-mailhog \
|
||||||
|
--network postfix-test-net \
|
||||||
|
-p 8025:8025 \
|
||||||
|
mailhog/mailhog \
|
||||||
|
MailHog -smtp-bind-addr 0.0.0.0:2525
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Start the postfix-pgsql container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name test-postfix \
|
||||||
|
--network postfix-test-net \
|
||||||
|
-p 25:25 \
|
||||||
|
-e MAIL_DOMAIN=example.com \
|
||||||
|
-e MYHOSTNAME=mx1.example.com \
|
||||||
|
-e POSTGRES_HOST=test-postgres \
|
||||||
|
-e POSTGRES_PORT=5432 \
|
||||||
|
-e POSTGRES_USER=relayuser \
|
||||||
|
-e POSTGRES_PASSWORD=testpassword \
|
||||||
|
-e POSTGRES_DB=relaydb \
|
||||||
|
-e SMTP_RELAY_HOST=test-mailhog \
|
||||||
|
-e SMTP_RELAY_PORT=2525 \
|
||||||
|
gitea.arnodo.fr/damien/postfix-pgsql:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm it started cleanly:
|
||||||
|
```bash
|
||||||
|
docker logs test-postfix
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Postfix master process line and no errors.
|
||||||
|
|
||||||
|
## 5. Send a test message
|
||||||
|
|
||||||
|
You can use `swaks` (Swiss Army Knife for SMTP) or plain `telnet`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With swaks (install: apt install swaks / brew install swaks)
|
||||||
|
swaks --to user@example.com \
|
||||||
|
--from sender@external.com \
|
||||||
|
--server 127.0.0.1 \
|
||||||
|
--port 25
|
||||||
|
|
||||||
|
# Or with telnet
|
||||||
|
telnet 127.0.0.1 25
|
||||||
|
EHLO test
|
||||||
|
MAIL FROM:<sender@external.com>
|
||||||
|
RCPT TO:<user@example.com>
|
||||||
|
DATA
|
||||||
|
Subject: Test
|
||||||
|
.
|
||||||
|
QUIT
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. View captured mail in MailHog
|
||||||
|
|
||||||
|
**Web UI** — if testing on a remote host, open an SSH tunnel first:
|
||||||
|
```bash
|
||||||
|
ssh -L 8025:localhost:8025 user@your-host
|
||||||
|
```
|
||||||
|
Then open `http://localhost:8025` in a browser.
|
||||||
|
|
||||||
|
**HTTP API** (no tunnel needed):
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8025/api/v2/messages | jq '.items[].Content.Headers.Subject'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker rm -f test-postfix test-mailhog test-postgres
|
||||||
|
docker network rm postfix-test-net
|
||||||
|
```
|
||||||
23
images/postfix-pgsql/tests/test-plan.md
Normal file
23
images/postfix-pgsql/tests/test-plan.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Test Plan: postfix-pgsql image
|
||||||
|
|
||||||
|
Manual test suite run against the image during initial validation on a Scaleway
|
||||||
|
test VM. All tests use the local environment described in `setup.md`.
|
||||||
|
|
||||||
|
**Convention for this table:**
|
||||||
|
- *Command(s)* are abbreviated; see `setup.md` for full environment setup.
|
||||||
|
- Add new rows at the bottom as the image evolves; do not rewrite existing rows
|
||||||
|
(failed + fixed tests are part of the history).
|
||||||
|
- Status: ✅ Pass · ❌ Fail · 🔧 Fixed (failed, root-caused, fix applied)
|
||||||
|
|
||||||
|
| # | Test | What was checked | Command(s) | Expected | Actual | Status |
|
||||||
|
|---|------|-----------------|------------|----------|--------|--------|
|
||||||
|
| 1 | Clean startup | No errors in container logs after start; Postfix master process running | `docker run ... gitea.arnodo.fr/damien/postfix-pgsql:latest` then `docker logs test-postfix` | Postfix `daemon started` line, no `fatal`/`panic` entries | Logs show `postfix/master[1]: daemon started -- version 3.7.x`, self-signed cert generation output, no errors | ✅ Pass |
|
||||||
|
| 2 | Missing required env var | Container exits with a clear error message when a required variable is absent | `docker run ... -e POSTGRES_HOST= ...` (POSTGRES_HOST set to empty) | Non-zero exit, `ERROR: required environment variable POSTGRES_HOST is not set` printed to stderr | Container exited immediately with the expected message | ✅ Pass |
|
||||||
|
| 3 | Template rendering correctness | Rendered `main.cf` contains substituted values, no leftover `${VAR}` placeholders | `docker exec test-postfix cat /etc/postfix/main.cf` | `myhostname = mx1.example.com`, `mydomain = example.com`, TLS paths resolved, no literal `${...}` strings | All values substituted; Postfix-native `$mydomain` references (no braces) correctly left untouched by envsubst | ✅ Pass |
|
||||||
|
| 4 | Self-signed TLS cert auto-generated | When no TLS_CERT_FILE/TLS_KEY_FILE are provided, a self-signed cert is generated and persisted | Start container without TLS vars; `docker exec test-postfix ls -l /etc/postfix/ssl/` | Files `snakeoil.pem` and `snakeoil.key` present; key mode 600 | Both files created on first start; `postconf smtpd_tls_cert_file` returned the correct path | ✅ Pass |
|
||||||
|
| 5 | Port 25 reachable | SMTP port 25 is accessible from outside the container and returns a valid Postfix banner | `telnet 127.0.0.1 25` | `220 mx1.example.com ESMTP Postfix` banner | Banner received as expected | ✅ Pass |
|
||||||
|
| 6 | Accepted domain → mail relayed | Mail sent to a recipient whose domain is in `relay_domain` is forwarded to the downstream SMTP target (MailHog) | `swaks --to user@example.com --server 127.0.0.1 --port 25`; check MailHog API | `250 2.0.0 Ok: queued` from Postfix; message appears in `GET /api/v2/messages` | Message delivered to MailHog and visible in the web UI | ✅ Pass |
|
||||||
|
| 7 | Unknown domain → relay denied | Mail sent to a domain not in `relay_domain` is rejected with relay access denied | `swaks --to user@notlisted.org --server 127.0.0.1 --port 25` | `554 5.7.1 Relay access denied` | Postfix returned `554 5.7.1 <user@notlisted.org>: Relay access denied` | ✅ Pass |
|
||||||
|
| 8 | PostgreSQL lookup failure — chroot DNS bug | pgsql-backed `relay_domains`/`transport_maps` lookups work correctly when Postgres is referenced by Docker container name | Send mail to `user@example.com` with `POSTGRES_HOST=test-postgres` (container name, not IP) | `250 Ok: queued`; no `451 Temporary lookup failure` in logs | **Without fix:** intermittent `451 4.3.0 Temporary lookup failure` — chrooted child processes (smtpd, trivial-rewrite) could not resolve `test-postgres` because they lack `/etc/resolv.conf` inside the chroot jail. **Fix applied:** added `postconf -F '*/*/chroot = n'` in `docker-entrypoint.sh` before `postfix start-fg`. After fix: lookups succeed consistently. | 🔧 Fixed |
|
||||||
|
| 9 | Container restart behavior | Config is re-rendered correctly on restart; pre-existing self-signed cert is reused without re-generation | `docker restart test-postfix`; `docker logs test-postfix` | No re-generation message for TLS cert; `main.cf` and pgsql maps fully substituted; Postfix starts without errors | Cert reuse message absent (new cert would log openssl output); Postfix started cleanly on restart | ✅ Pass |
|
||||||
|
| 10 | No credential leakage in logs | Postgres password does not appear anywhere in `docker logs` output | `docker logs test-postfix 2>&1 \| grep testpassword` | No output (password not printed) | No lines containing the password; envsubst substitution into config files is silent | ✅ Pass |
|
||||||
Reference in New Issue
Block a user