SMTP invoice email adapter
The SMTP adapter is an optional transport behind the durable invoice-email delivery state machine. It uses the bundled mail 2.9.1 and net-smtp 0.5.1 gems. It does not implement delivery-status lookup or reconciliation: ordinary SMTP cannot authoritatively query a prior submission by the client Message-ID.
Activation and configuration
Configure SMTP in Systemeinstellungen at /admin/system_settings using a system administrator account. Stored global configuration takes precedence over environment variables, including a stored disabled state. Global SMTP and the merchant’s InvoiceEmailDeliveryEnabled setting must both be enabled. See /docs/system-smtp-settings for password handling, permissions and auditing.
Only while no global database configuration has been saved, all three fallback gates apply:
-
global
BILLING_INVOICE_EMAIL_DELIVERY_ENABLED=true; -
merchant setting
InvoiceEmailDeliveryEnabled=true; -
BILLING_INVOICE_EMAIL_ADAPTER=smtpwith a valid adapter configuration.
The environment fallback constructor reads:
| Variable | Requirement |
|---|---|
BILLING_SMTP_HOST |
Required DNS hostname; no scheme, path, credentials, or port. |
BILLING_SMTP_PORT |
Optional, defaults to 587; must be 1–65535. |
BILLING_SMTP_HELO_DOMAIN |
Required DNS hostname used for EHLO/HELO. |
BILLING_SMTP_USERNAME |
Required; passed only to SMTP authentication. |
BILLING_SMTP_PASSWORD |
Required; passed only to SMTP authentication. |
BILLING_SMTP_ALLOWED_FROM |
Required exact sender address for this installation. |
BILLING_SMTP_TLS_MODE |
starttls by default or tls; no plaintext/automatic downgrade mode. |
Billing::SmtpInvoiceEmailAdapter.from_env validates configuration without opening DNS or a socket. The object redacts its inspection string. Do not enable Net::SMTP debug output: it can expose authentication and message contents.
TLS and authentication
Submission uses either mandatory STARTTLS (starttls: :always) or implicit TLS. Both modes enable certificate and hostname verification and require TLS 1.2 or newer. Open and read timeouts are bounded; a write timeout is set only when the injected transport exposes that capability. Authentication is SASL PLAIN inside the verified TLS session; LOGIN and CRAM-MD5 are not enabled.
The implementation follows the installed/upstream interfaces:
MIME contract
The adapter creates one multipart message with fixed From, To, Subject, Date, Message-ID, and MIME headers; a UTF-8 plain-text body; and exactly one base64 application/pdf attachment. The stable client Message-ID derives from the durable dispatch ID. No Bcc, HTML, custom headers, filesystem path, credential, or raw provider data is added.
The adapter receives attachment bytes only in memory after the delivery service has verified the immutable IssuedDocument and manifest. It never persists the generated raw MIME message.
Outcomes
-
Sent:send_messagereceived the SMTP server’s successful final DATA response. This proves only server/transport acceptance. It does not prove inbox delivery or the later Frisbii stateZugestellt. -
Rejected: a bounded SMTP authentication, policy, recipient, or server response explicitly rejected submission. Server response text is discarded and replaced by a fixed local code. -
Unknown: invocation began but no authoritative final result was obtained, or an unknown SMTP response occurred. The delivery state machine prohibits automatic resend.
If send_message returns successfully and closing the session later fails, the result remains Sent: the server already accepted the DATA transaction. DNS, TCP, TLS, or authentication failures before message invocation are fixed rejections. The adapter deliberately exposes no reconcile method; an unknown SMTP submission requires operator/provider evidence outside this adapter.
Production gates
Keep the adapter disabled until credentials are provisioned through the deployment secret store, the allowed sender and DNS/TLS policy are reviewed, and an isolated staging rehearsal covers acceptance, explicit rejection, timeout after DATA begins, close failure, bounce processing, and unknown-state operations. Bounce and delivered notifications require a separate inbound provider integration and must not rewrite Sent without durable evidence.
Local verification
BILLING_DB_PORT=55440 sh bin/test-local \ test/services/billing/invoice_email_delivery_test.rb \ test/services/billing/invoice_email_delivery_concurrency_test.rb \ test/services/billing/smtp_invoice_email_adapter_test.rb \ test/jobs/invoice_email_delivery_job_test.rb 24 runs, 254 assertions, 0 failures, 0 errors, 0 skips
The adapter tests use an injected session/factory only. They parse the generated MIME message, round-trip the base64 PDF bytes, inspect TLS and authentication arguments, and simulate explicit SMTP rejection, lost response, mandatory-STARTTLS failure, and failure after accepted DATA. No DNS lookup, socket, credential, provider, production address, or real email is used.