fix(email): announce a configurable SMTP EHLO name #508

Merged
Claude-Run merged 1 commit from fix/BUNYIP-507-configurable-smtp-ehlo-name into main 2026-08-11 02:13:39 +02:00
Member

Both SMTP paths let lettre pick the EHLO name, so with the default hostname feature ClientId::default() resolved to the OS hostname, which inside a container is the short container id (EHLO 16c98fb0ffe1 on the Stalwart relay). A container id is not a FQDN: relays that enforce reject-non-fqdn, that match EHLO against forward/reverse DNS, or that score it for spam reject or penalise the mail, and the name changes on every container recreate so it has no diagnostic value either.

EmailConfig gains smtp_ehlo_name (from a new SMTP_EHLO_NAME, trimmed, empty treated as unset) and EmailConfig::ehlo_name(), which resolves SMTP_EHLO_NAME -> host of base_url (APP_URL) -> domain of from_email -> lettre's default. The fallback chain makes this a no-config fix: APP_URL is already a real FQDN in every environment, so existing deployments stop announcing the container id on upgrade. An IP source goes on the wire as an address literal ([10.1.2.3]) per RFC 5321. The EHLO identity is deployment/network configuration rather than a per-tenant email setting, so it stays env-derived like base_url and needs no email_config column or migration; it is not a secret, so no SECRET_MAP entry.

Both build_transport arms now call .hello_name(...), test_connection uses the same resolved name for connect_tokio1 and conn.starttls(...), and the resolved name is logged once at info when the transport is built (so it also prints on every admin reload).

Completeness sweep, every SMTP client-id / connection construction site in the workspace:

Site Classification
services/email.rs:87-105 build_transport, both TLS arms compliant: .hello_name(config.ehlo_name())
services/email.rs:161 test_connection hello compliant: config.ehlo_name()
services/email.rs:172,185 AsyncSmtpConnection::connect_tokio1 compliant: both take that hello
services/email.rs:197 conn.starttls(tls_params, &hello) compliant: same hello
services/email.rs:1426,1446 mock-relay test connections compliant: config_with_smtp(..).ehlo_name()
services/email.rs:1477 new on-the-wire EHLO test compliant: asserts the relay saw EHLO mail.example.com
config.rs:371 ClientId::default() compliant: the resolver's last fallback, the one place it may appear
rest of the workspace (bunyip-api, bunyip-web, bunyip-oci, bunyip-oidc) N/A: lettre is a bunyip-domain-only dependency, no other crate opens an SMTP session

services::email::tests::no_smtp_session_falls_back_to_the_container_hostname fails the build if ClientId::default() reappears in email.rs or if a relay builder stops setting the hello name, so the removal is enforced mechanically.

#BUNYIP-507

Both SMTP paths let lettre pick the EHLO name, so with the default `hostname` feature `ClientId::default()` resolved to the OS hostname, which inside a container is the short container id (`EHLO 16c98fb0ffe1` on the Stalwart relay). A container id is not a FQDN: relays that enforce `reject-non-fqdn`, that match EHLO against forward/reverse DNS, or that score it for spam reject or penalise the mail, and the name changes on every container recreate so it has no diagnostic value either. `EmailConfig` gains `smtp_ehlo_name` (from a new `SMTP_EHLO_NAME`, trimmed, empty treated as unset) and `EmailConfig::ehlo_name()`, which resolves `SMTP_EHLO_NAME` -> host of `base_url` (`APP_URL`) -> domain of `from_email` -> lettre's default. The fallback chain makes this a no-config fix: `APP_URL` is already a real FQDN in every environment, so existing deployments stop announcing the container id on upgrade. An IP source goes on the wire as an address literal (`[10.1.2.3]`) per RFC 5321. The EHLO identity is deployment/network configuration rather than a per-tenant email setting, so it stays env-derived like `base_url` and needs no `email_config` column or migration; it is not a secret, so no `SECRET_MAP` entry. Both `build_transport` arms now call `.hello_name(...)`, `test_connection` uses the same resolved name for `connect_tokio1` and `conn.starttls(...)`, and the resolved name is logged once at `info` when the transport is built (so it also prints on every admin reload). Completeness sweep, every SMTP client-id / connection construction site in the workspace: | Site | Classification | | --- | --- | | `services/email.rs:87-105` `build_transport`, both TLS arms | compliant: `.hello_name(config.ehlo_name())` | | `services/email.rs:161` `test_connection` hello | compliant: `config.ehlo_name()` | | `services/email.rs:172,185` `AsyncSmtpConnection::connect_tokio1` | compliant: both take that `hello` | | `services/email.rs:197` `conn.starttls(tls_params, &hello)` | compliant: same `hello` | | `services/email.rs:1426,1446` mock-relay test connections | compliant: `config_with_smtp(..).ehlo_name()` | | `services/email.rs:1477` new on-the-wire EHLO test | compliant: asserts the relay saw `EHLO mail.example.com` | | `config.rs:371` `ClientId::default()` | compliant: the resolver's last fallback, the one place it may appear | | rest of the workspace (`bunyip-api`, `bunyip-web`, `bunyip-oci`, `bunyip-oidc`) | N/A: lettre is a `bunyip-domain`-only dependency, no other crate opens an SMTP session | `services::email::tests::no_smtp_session_falls_back_to_the_container_hostname` fails the build if `ClientId::default()` reappears in `email.rs` or if a relay builder stops setting the hello name, so the removal is enforced mechanically. #BUNYIP-507
fix(email): announce a configurable SMTP EHLO name
All checks were successful
E2E / PR gate (pull_request) Successful in 23s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m39s
Create release / Create release from merged PR (pull_request) Has been skipped
e8ee656db6
Both SMTP paths let lettre pick the EHLO name, so with the default `hostname` feature `ClientId::default()` resolved to the OS hostname, which inside a container is the short container id (`EHLO 16c98fb0ffe1` on the Stalwart relay). A container id is not a FQDN: relays that enforce `reject-non-fqdn`, that match EHLO against forward/reverse DNS, or that score it for spam reject or penalise the mail, and the name changes on every container recreate so it has no diagnostic value either.

`EmailConfig` gains `smtp_ehlo_name` (from a new `SMTP_EHLO_NAME`, trimmed, empty treated as unset) and `EmailConfig::ehlo_name()`, which resolves `SMTP_EHLO_NAME` -> host of `base_url` (`APP_URL`) -> domain of `from_email` -> lettre's default. The fallback chain makes this a no-config fix: `APP_URL` is already a real FQDN in every environment, so existing deployments stop announcing the container id on upgrade. An IP source goes on the wire as an address literal (`[10.1.2.3]`) per RFC 5321. The EHLO identity is deployment/network configuration rather than a per-tenant email setting, so it stays env-derived like `base_url` and needs no `email_config` column or migration; it is not a secret, so no `SECRET_MAP` entry.

Both `build_transport` arms now call `.hello_name(...)`, `test_connection` uses the same resolved name for `connect_tokio1` and `conn.starttls(...)`, and the resolved name is logged once at `info` when the transport is built (so it also prints on every admin reload).

Completeness sweep, every SMTP client-id / connection construction site in the workspace:

| Site | Classification |
| --- | --- |
| `services/email.rs:87-105` `build_transport`, both TLS arms | compliant: `.hello_name(config.ehlo_name())` |
| `services/email.rs:161` `test_connection` hello | compliant: `config.ehlo_name()` |
| `services/email.rs:172,185` `AsyncSmtpConnection::connect_tokio1` | compliant: both take that `hello` |
| `services/email.rs:197` `conn.starttls(tls_params, &hello)` | compliant: same `hello` |
| `services/email.rs:1426,1446` mock-relay test connections | compliant: `config_with_smtp(..).ehlo_name()` |
| `services/email.rs:1477` new on-the-wire EHLO test | compliant: asserts the relay saw `EHLO mail.example.com` |
| `config.rs:371` `ClientId::default()` | compliant: the resolver's last fallback, the one place it may appear |
| rest of the workspace (`bunyip-api`, `bunyip-web`, `bunyip-oci`, `bunyip-oidc`) | N/A: lettre is a `bunyip-domain`-only dependency, no other crate opens an SMTP session |

`services:📧:tests::no_smtp_session_falls_back_to_the_container_hostname` fails the build if `ClientId::default()` reappears in `email.rs` or if a relay builder stops setting the hello name, so the removal is enforced mechanically.

#BUNYIP-507
Claude-Run deleted branch fix/BUNYIP-507-configurable-smtp-ehlo-name 2026-08-11 02:13:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!508
No description provided.