package notification import ( "context" "log/slog" ) // EmailBackend names the chosen email delivery channel for telemetry + // startup logging. Mostly a debugging aid — code paths don't branch on // this value. type EmailBackend string const ( BackendResend EmailBackend = "resend" BackendSMTP EmailBackend = "smtp" BackendSES EmailBackend = "ses" BackendLog EmailBackend = "log" ) // EmailSenderConfig collects every email-related env var so the picker // has a single, ordered place to decide which backend wins. Priority is // Resend > SMTP > SES > Log — the first one with non-empty creds is used. type EmailSenderConfig struct { Resend ResendConfig SMTP SMTPConfig SES SESConfig } // CombinedEmailSender satisfies both the auth.EmailSender interface (for // verification + reset emails) and GuestEmailDispatcher (for invitation, // confirmation, reminder). One concrete value handles both audiences so // callers don't end up with two configurations. type CombinedEmailSender interface { SendVerification(ctx context.Context, to, name, link string) error SendPasswordReset(ctx context.Context, to, name, link string) error SendGuest(ctx context.Context, to, subject string, name TemplateName, data map[string]any) (string, error) } // PickEmailSender returns the configured email sender + which backend was // chosen. Falls back to a logger stub if nothing is configured, so the // service stays bootable in stripped-down dev environments. func PickEmailSender(ctx context.Context, cfg EmailSenderConfig, tpls *Templates, logger *slog.Logger) (CombinedEmailSender, EmailBackend, error) { switch { case cfg.Resend.APIKey != "": s, err := NewResendEmailSender(cfg.Resend, tpls) if err != nil { return nil, "", err } return s, BackendResend, nil case cfg.SMTP.Host != "": s, err := NewSMTPEmailSender(cfg.SMTP, tpls) if err != nil { return nil, "", err } return s, BackendSMTP, nil case cfg.SES.FromEmail != "": s, err := NewSESEmailSender(ctx, cfg.SES, tpls) if err != nil { return nil, "", err } return s, BackendSES, nil } return &logCombinedSender{logger: logger, tpls: tpls}, BackendLog, nil } // logCombinedSender is the dev fallback. Verification + reset emails come // through as structured log lines (preserving the Block A behaviour); // guest emails get rendered + dumped so engineers can eyeball the output. type logCombinedSender struct { logger *slog.Logger tpls *Templates } func (l *logCombinedSender) SendVerification(_ context.Context, to, name, link string) error { l.logger.Info("auth email (stub): verification", "to", to, "name", name, "link", link) return nil } func (l *logCombinedSender) SendPasswordReset(_ context.Context, to, name, link string) error { l.logger.Info("auth email (stub): password reset", "to", to, "name", name, "link", link) return nil } func (l *logCombinedSender) SendGuest(_ context.Context, to, subject string, name TemplateName, data map[string]any) (string, error) { if data == nil { data = map[string]any{} } data["Subject"] = subject _, text, err := l.tpls.Render(name, data) if err != nil { return "", err } l.logger.Info("guest email (stub)", "to", to, "subject", subject, "template", string(name), "text_body", text, ) return "log:" + string(name), nil }