feat(email): SMTP mailer behind the EmailService seam (EMAIL_PROVIDER=smtp); Resend stays the hosted default (#1746)

SmtpEmailService (nodemailer 9.0.5, exact-pinned) behind the existing EmailService seam. Provider resolution: EMAIL_PROVIDER wins, else RESEND_API_KEY selects Resend (hosted byte-identical), else SMTP_HOST selects SMTP. From header is built exactly like the Resend service after #1956 (no 'via <app>', fromAddress honored, platform-sender retry). STARTTLS is required by default (requireTLS) with SMTP_REQUIRE_TLS=false as an explicit opt-out for a plaintext LAN relay. Docs, env examples and the generated extension registry updated.
This commit is contained in:
Jakob Wennberg
2026-08-30 11:54:47 +02:00
committed by GitHub
parent 8ec12fefc7
commit 523fba0419
21 changed files with 716 additions and 29 deletions
@@ -26,7 +26,7 @@
* failure never blocks the send; it surfaces as a PAYMENT_LINK_FAILED
* warning on the response once the email is delivered.
* 7. Final PDF render with the real number.
* 8. Email send via Resend (the email extension). Fail → 502
* 8. Email send via the email extension (Resend or SMTP). Fail → 502
* INVOICE_SEND_PROVIDER_FAILED. The number IS consumed at this point;
* same orphan-window as :mark-sent (architecturally tracked).
* 9. POINT OF NO RETURN. Steps below are best-effort; failures surface
@@ -123,14 +123,14 @@ registerEndpoint({
path: '/api/v1/companies/:companyId/invoices/:id/send',
summary: 'Send a draft invoice to the customer by email.',
description:
'The full send pipeline: preflight PDF render → allocate F-series number atomically → final PDF render → email via Resend (PDF attachment, copy to company) → flip status to sent → post journal entry (real invoice, unless kontantmetoden or defer_invoice_booking) → archive PDF as underlag → emit invoice.sent. Email failure is a hard 502 before state changes; post-email failures surface as warnings but the invoice IS marked sent.',
'The full send pipeline: preflight PDF render → allocate F-series number atomically → final PDF render → email via the email extension (Resend or SMTP; PDF attachment, copy to company) → flip status to sent → post journal entry (real invoice, unless kontantmetoden or defer_invoice_booking) → archive PDF as underlag → emit invoice.sent. Email failure is a hard 502 before state changes; post-email failures surface as warnings but the invoice IS marked sent.',
useWhen:
'You want Accounted to deliver the invoice to the customer via email. For invoices delivered through another channel (Peppol, postal, own SMTP) use :mark-sent instead.',
doNotUseFor:
'Re-sending an already-sent invoice (returns 409 INVOICE_UPDATE_NOT_DRAFT). Sending a delivery note (no F-series lifecycle). Sending a credit note (use the :credit endpoint to issue the kreditfaktura; subsequent re-send of the credit note via :mark-sent is the supported path).',
pitfalls: [
'Idempotency-Key is mandatory.',
'Email service must be configured: without RESEND_API_KEY + RESEND_FROM_EMAIL the endpoint returns 503 INVOICE_SEND_EMAIL_NOT_CONFIGURED.',
'Email service must be configured: without RESEND_API_KEY + RESEND_FROM_EMAIL (or an SMTP relay via EMAIL_PROVIDER=smtp) the endpoint returns 503 INVOICE_SEND_EMAIL_NOT_CONFIGURED.',
'Customer must have an email address. 400 INVOICE_SEND_NO_CUSTOMER_EMAIL otherwise.',
'A cancelled invoice is rejected (400 INVOICE_SEND_CANCELLED): its F-series number is preserved for compliance but the document is not a valid faktura.',
'Email failure before the status flip leaves the F-series number consumed but the invoice in `draft` status. Same orphan window as :mark-sent (architecturally tracked, matches internal route).',