package email import ( "fmt" "net/smtp" "os" "go.uber.org/zap" ) // EmailSender interface pour l'envoi d'emails type EmailSender interface { Send(to, subject, body string) error SendTemplate(to, template string, data map[string]interface{}) error } // SMTPConfig contient la configuration SMTP type SMTPConfig struct { Host string Port string Username string Password string From string FromName string } // SMTPEmailSender implémente EmailSender avec SMTP réel type SMTPEmailSender struct { config SMTPConfig logger *zap.Logger } // NewSMTPEmailSender crée un nouveau sender SMTP func NewSMTPEmailSender(config SMTPConfig, logger *zap.Logger) *SMTPEmailSender { return &SMTPEmailSender{ config: config, logger: logger, } } // Send envoie un email via SMTP func (s *SMTPEmailSender) Send(to, subject, body string) error { // Si pas de config SMTP, log seulement (dev mode) if s.config.Host == "" { s.logger.Info("SMTP not configured, email would be sent", zap.String("to", to), zap.String("subject", subject), ) return nil } // SMTP auth auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host) // Email headers avec format correct fromHeader := s.config.From if s.config.FromName != "" { fromHeader = fmt.Sprintf("%s <%s>", s.config.FromName, s.config.From) } msg := []byte(fmt.Sprintf("From: %s\r\n"+ "To: %s\r\n"+ "Subject: %s\r\n"+ "MIME-Version: 1.0\r\n"+ "Content-Type: text/html; charset=UTF-8\r\n"+ "\r\n"+ "%s", fromHeader, to, subject, body)) // Send email addr := fmt.Sprintf("%s:%s", s.config.Host, s.config.Port) err := smtp.SendMail(addr, auth, s.config.From, []string{to}, msg) if err != nil { return fmt.Errorf("failed to send email via SMTP: %w", err) } s.logger.Info("Email sent successfully", zap.String("to", to), zap.String("subject", subject), ) return nil } // SendTemplate envoie un email avec un template // Pour l'instant, cette méthode appelle Send avec le body généré // L'implémentation complète avec template engine sera dans email_job.go func (s *SMTPEmailSender) SendTemplate(to, template string, data map[string]interface{}) error { // Cette méthode sera utilisée par EmailJob qui gère le rendu des templates // Pour l'instant, on délègue au template renderer return fmt.Errorf("SendTemplate not implemented directly, use EmailJob instead") } // v1.0.6 — single SMTP env schema. Before this commit, two services read // different env names for the same fields: // // internal/email/sender.go internal/services/email_service.go // SMTP_USERNAME SMTP_USER // SMTP_FROM FROM_EMAIL // SMTP_FROM_NAME FROM_NAME // // Both now consume LoadSMTPConfigFromEnv. The canonical names are SMTP_* // everywhere. The old names are still read as a deprecation fallback with // a one-line warning so existing deployments don't break mid-rollout — to // be removed in v1.1.0. // LoadSMTPConfigFromEnv reads the canonical SMTP_* env vars. // No defaults are injected: Host/Port come straight from env. A caller // seeing Host=="" must treat SMTP as unconfigured and log-only (matches // the historic dev behavior). Dev defaults (MailHog on localhost:1025) // live in `veza-backend-api/.env.template` — this keeps the runtime // honest and lets prod fail fast on a missing env. func LoadSMTPConfigFromEnv() SMTPConfig { return LoadSMTPConfigFromEnvWithLogger(nil) } // LoadSMTPConfigFromEnvWithLogger is the logger-aware variant — pass your // package logger so deprecation warnings land in structured logs. func LoadSMTPConfigFromEnvWithLogger(logger *zap.Logger) SMTPConfig { return SMTPConfig{ Host: os.Getenv("SMTP_HOST"), Port: os.Getenv("SMTP_PORT"), Username: resolveDeprecated(logger, "SMTP_USERNAME", "SMTP_USER"), Password: os.Getenv("SMTP_PASSWORD"), From: resolveDeprecated(logger, "SMTP_FROM", "FROM_EMAIL"), FromName: resolveDeprecated(logger, "SMTP_FROM_NAME", "FROM_NAME"), } } // resolveDeprecated returns the value of `canonical` if set, otherwise // falls back to `deprecated` (emitting a warning). Empty string if neither // is set. Exported at package scope so it's unit-testable in isolation. func resolveDeprecated(logger *zap.Logger, canonical, deprecated string) string { if v := os.Getenv(canonical); v != "" { return v } if v := os.Getenv(deprecated); v != "" { if logger != nil { logger.Warn( "Deprecated SMTP env var in use — migrate to the canonical name", zap.String("deprecated", deprecated), zap.String("canonical", canonical), zap.String("remove_in", "v1.1.0"), ) } return v } return "" }