From 64ba21795bc97530111edc32931a5d3486a45eb5 Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Fri, 4 Sep 2026 08:40:36 +0100 Subject: [PATCH] Bundle a Mailpit catcher and default Compose to SMTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a mailpit service (axllent/mailpit, ~15 MB, in-memory) to docker-compose.yml — chosen over the unmaintained MailHog for the same footprint. It has no profile, so `docker compose up -d` starts it alongside the API; the app defaults to MAIL_TRANSPORT=smtp -> mailpit:1025 (no auth/TLS) and mail is read at http://localhost:8025. Also change the default MAIL_FROM to no-reply@todo.test: PHPMailer v7 rejects the dotless no-reply@localhost as an invalid address. Co-Authored-By: Claude Sonnet 5 --- .env.example | 2 +- README.md | 15 ++++++++++----- docker-compose.yml | 25 +++++++++++++++++-------- src/Support/Config.php | 2 +- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index ae6980d..8e1a61a 100644 --- a/.env.example +++ b/.env.example @@ -28,7 +28,7 @@ APP_URL=http://localhost:5173 # smtp — the SMTP server configured below # log — append messages to MAIL_LOG_PATH instead of sending (dev/test) MAIL_TRANSPORT=mail -MAIL_FROM=no-reply@localhost +MAIL_FROM=no-reply@todo.test MAIL_FROM_NAME=Todo List MAIL_LOG_PATH=storage/mail.log diff --git a/README.md b/README.md index 9e4de45..46498e3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ docker compose up -d This builds a PHP 8.3 + Apache image, applies migrations, and serves the API at (e.g. `curl http://localhost:8080/api/health`). +- A **[Mailpit](https://mailpit.axllent.org/)** container (the maintained MailHog + successor — one ~15 MB Go binary, messages kept in memory) also starts. The API + sends all email to it; read it at . Set + `MAIL_TRANSPORT=mail` or `=smtp` (with `MAIL_SMTP_*`) to send for real. - The project directory is bind-mounted into the container, so editing PHP source takes effect without a rebuild (within ~2s, due to the opcache revalidation interval). `vendor/` is used from the host — run `composer` @@ -103,13 +107,14 @@ environment). See [.env.example](.env.example). | `JWT_TTL` | `86400` | Token lifetime in seconds | | `APP_URL` | `http://localhost:5173` | Frontend base URL used to build magic links | | `MAIL_TRANSPORT` | `mail` | `mail` (PHP `mail()`), `smtp`, or `log` (append to a file) | -| `MAIL_FROM` / `MAIL_FROM_NAME` | `no-reply@localhost` / `Todo List` | Envelope sender | +| `MAIL_FROM` / `MAIL_FROM_NAME` | `no-reply@todo.test` / `Todo List` | Envelope sender | | `MAIL_LOG_PATH` | `storage/mail.log` | Where `log` transport writes | | `MAIL_SMTP_HOST` / `_PORT` / `_USERNAME` / `_PASSWORD` / `_ENCRYPTION` | — / `587` / — / — / `tls` | Used only when `MAIL_TRANSPORT=smtp` | -SMTP is opt-in; without it the API falls back to PHP's `mail()`. The Docker -Compose setup sets `MAIL_TRANSPORT=log` (the container has no MTA) — read the -links with `docker compose exec app cat /var/www/storage/mail.log`. +Standalone, SMTP is opt-in and the API otherwise falls back to PHP's `mail()`. +Under Docker Compose the default is `MAIL_TRANSPORT=smtp` pointed at the bundled +Mailpit container (`mailpit:1025`, no auth/TLS); open to +read what was "sent". ## API @@ -358,7 +363,7 @@ src/Repository/ Database access (User, EmailVerification, TodoList, src/Support/Validator.php Request-body validation helper migrations/*.sql Schema, applied by bin/migrate.php Dockerfile PHP 8.3 + Apache image -docker-compose.yml One-command local stack (API; web via --profile frontend) +docker-compose.yml Local stack: API + Mailpit; web via --profile frontend docker/ Apache vhost + container entrypoint web/ Vue 3 + TypeScript + Vite PWA frontend ``` diff --git a/docker-compose.yml b/docker-compose.yml index 1c96b04..f1d5074 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,21 +14,30 @@ services: JWT_TTL: "${JWT_TTL:-86400}" # Magic links point at the frontend dev server. APP_URL: "${APP_URL:-http://localhost:5173}" - # The container has no MTA, so log emails to a file by default. View them with: - # docker compose exec app cat /var/www/storage/mail.log - # Set to "smtp" (with MAIL_SMTP_*) or "mail" to actually send. - MAIL_TRANSPORT: "${MAIL_TRANSPORT:-log}" - MAIL_FROM: "${MAIL_FROM:-no-reply@localhost}" - MAIL_SMTP_HOST: "${MAIL_SMTP_HOST:-}" - MAIL_SMTP_PORT: "${MAIL_SMTP_PORT:-587}" + # Deliver to the Mailpit catcher below; read mail at http://localhost:8025. + MAIL_TRANSPORT: "${MAIL_TRANSPORT:-smtp}" + MAIL_FROM: "${MAIL_FROM:-no-reply@todo.test}" + MAIL_SMTP_HOST: "${MAIL_SMTP_HOST:-mailpit}" + MAIL_SMTP_PORT: "${MAIL_SMTP_PORT:-1025}" MAIL_SMTP_USERNAME: "${MAIL_SMTP_USERNAME:-}" MAIL_SMTP_PASSWORD: "${MAIL_SMTP_PASSWORD:-}" - MAIL_SMTP_ENCRYPTION: "${MAIL_SMTP_ENCRYPTION:-tls}" + MAIL_SMTP_ENCRYPTION: "${MAIL_SMTP_ENCRYPTION:-none}" volumes: # Live source: edit on the host, no image rebuild needed. - .:/var/www/html # Persistent storage, kept outside /var/www/html to avoid a nested mount. - storage:/var/www/storage + depends_on: + - mailpit + restart: unless-stopped + + # Development mail catcher (Mailpit — the maintained MailHog successor). ~15 MB, + # single Go binary, messages held in memory. Web UI: http://localhost:8025 + mailpit: + image: axllent/mailpit:v1.31 + ports: + - "8025:8025" # web UI + REST API + - "1025:1025" # SMTP (also reachable in-network as mailpit:1025) restart: unless-stopped # Optional Vite dev server. Start it with: docker compose --profile frontend up -d diff --git a/src/Support/Config.php b/src/Support/Config.php index 8a9c248..26d5bb5 100644 --- a/src/Support/Config.php +++ b/src/Support/Config.php @@ -50,7 +50,7 @@ final class Config $mail = new MailConfig( transport: strtolower(self::env('MAIL_TRANSPORT', 'mail')), - fromAddress: self::env('MAIL_FROM', 'no-reply@localhost'), + fromAddress: self::env('MAIL_FROM', 'no-reply@todo.test'), fromName: self::env('MAIL_FROM_NAME', 'Todo List'), logPath: $mailLogPath, smtpHost: self::env('MAIL_SMTP_HOST'),