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 SendCollaboratorInvite(ctx context.Context, to, inviterName, eventName, role, 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) SendCollaboratorInvite(_ context.Context, to, inviterName, eventName, role, link string) error { l.logger.Info("auth email (stub): collaborator invite", "to", to, "inviter", inviterName, "event", eventName, "role", role, "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 }