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
+1 -1
View File
@@ -63,7 +63,7 @@ extensions/
calendar/ ← Kalender: month/week/day views
cloud-backup/ ← Molnsynkronisering: sync the säkerhetsbackup to the user's own cloud storage
document-extraction/ ← AI-extrahering av underlag: reads receipts and invoices, fills supplier/amount/VAT/date
email/ ← E-post (Resend): invoices and reminders by e-mail
email/ ← E-post (Resend or SMTP): invoices and reminders by e-mail
enable-banking/ ← Bankintegration (PSD2): automatic bank transaction sync
invoice-inbox/ ← Dokumentinkorg: forward supplier invoices to a unique address
mail/ ← Brevlådor: lets Kvittojakten (receipt hunt) search the user's mailboxes
+23 -1
View File
@@ -298,12 +298,34 @@ npx tsx scripts/smoke-ai.ts ./receipt.pdf # also runs document extraction
### Email (Invoice Sending, Invitations and Reminders)
Outbound mail (invoices, reminders, payslips) goes through one of two providers. Auth/account mail is sent by Supabase Auth and is not affected.
**Option 1: Resend** (what hosted runs):
```bash
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=noreply@your-domain.com
```
Requires a [Resend](https://resend.com) account with a verified sender domain. Without this, invoices can still be generated as PDFs but cannot be emailed.
Requires a [Resend](https://resend.com) account with a verified sender domain.
**Option 2: your own SMTP relay** (a Swedish mail provider, a Microsoft 365 / Google Workspace relay, Postfix on the host):
```bash
EMAIL_PROVIDER=smtp
SMTP_HOST=smtp.example.se
SMTP_PORT=587 # default 587; 465 with SMTP_SECURE=true
SMTP_SECURE=false # true = implicit TLS, false = STARTTLS required (set SMTP_REQUIRE_TLS=false only for a plaintext LAN relay)
SMTP_USER=... # optional for an internal relay
SMTP_PASS=...
SMTP_FROM_EMAIL=faktura@your-domain.se
# SMTP_REQUIRE_TLS=false # only for a plaintext relay on a trusted LAN: without it a relay that cannot STARTTLS fails the send instead of leaking credentials and invoice PDFs in cleartext
# SMTP_TLS_REJECT_UNAUTHORIZED=false # only for a LAN relay with a self-signed certificate
```
`EMAIL_PROVIDER` is optional: with a `RESEND_API_KEY` present Resend is used, otherwise `SMTP_HOST` selects SMTP, so adding SMTP variables next to an existing Resend key never moves mail by accident. Set it explicitly when both are configured. The From header is built identically on both providers (the company or brand name as display name, the platform address from `RESEND_FROM_EMAIL` or `SMTP_FROM_EMAIL` unless the company has a verified sending domain); the delivery-status webhook is Resend-only.
Without either, invoices can still be generated as PDFs but cannot be emailed.
**Invitations do not require Resend.** When no mail provider is configured, the invite is still created and the accept link is returned to the inviter in the app (a copy button under the pending invitations list, plus a warn-level log record whose msg is `email service not configured: invite email skipped`; the Docker image logs JSON, so grep for the message text, not a `WARN` prefix; the token is never logged). Share the link manually; it is valid until the invitation expires. Resend (or plain SMTP once [#1746](https://github.com/erp-mafia/accounted/pull/1746) lands) is only needed if you want the invitation mailed automatically. There is no re-send for company invitations: revoke and invite again for a new link.