feat(account): brand and harden the email pipeline

Swap the net/smtp mailer for go-mail behind the existing Mailer seam: the
Message struct now carries a text + HTML body, TLS mode is chosen from the port
(implicit TLS on 465, else mandatory STARTTLS), a dial timeout bounds the
synchronous send, and no client certificate is needed. Add a branded, image-free,
mobile-friendly ru/en HTML template (with a plain-text alternative) rendering a
large readable code and an ignore-notice footer with a landing link.

Add BACKEND_PUBLIC_BASE_URL config (the canonical origin for the email footer
link, never the request Host — anti-injection), required when a relay is
configured.

Fix the email-login address squat: ProvisionEmail creates the account flagged
is_guest until the code is confirmed, so an abandoned login is reaped like any
guest and its address freed; confirming (login or link) clears the flag. Seed the
new account's language from the client, plumbed through the email-login request.

The confirm deeplink, its transport surface and the send rate-limit land in
follow-up work.
This commit is contained in:
Ilia Denisov
2026-07-03 02:25:28 +02:00
parent d5fbaa3034
commit 3877b23894
14 changed files with 464 additions and 96 deletions
+1 -1
View File
@@ -174,7 +174,7 @@ func run(ctx context.Context, cfg config.Config, logger *zap.Logger) error {
// Lobby & social domains. Their REST and stream surface lives in the gateway,
// so they are handed to the server (like the route groups) for the handlers.
mailer := newMailer(cfg.SMTP, logger)
emails := account.NewEmailService(accounts, mailer)
emails := account.NewEmailService(accounts, mailer, cfg.PublicBaseURL)
// Account linking & merge: the orchestrator over the account, merge and
// session layers. Wired to the /api/v1/user/link REST surface below.
links := link.NewService(emails, accounts, accountmerge.NewMerger(db), sessions)