// Package email provides email notification service for the SYNTREX SOC platform. // Supports Resend (resend.com) as the primary transactional email provider. package email import ( "bytes" "encoding/json" "fmt" "io" "log/slog" "net/http" "time" ) // Sender defines the email sending interface. type Sender interface { Send(to, subject, htmlBody string) error } // StubSender logs emails instead of sending them (development mode). type StubSender struct{} func (s *StubSender) Send(to, subject, htmlBody string) error { slog.Info("email: stub send", "to", to, "subject", subject, "body_len", len(htmlBody)) return nil } // ResendSender sends emails via Resend API (https://resend.com). type ResendSender struct { apiKey string fromAddr string client *http.Client } // NewResendSender creates a Resend email sender. // apiKey format: "re_xxxxxxxxx" // fromAddr example: "SYNTREX " func NewResendSender(apiKey, fromAddr string) *ResendSender { return &ResendSender{ apiKey: apiKey, fromAddr: fromAddr, client: &http.Client{ Timeout: 10 * time.Second, }, } } func (s *ResendSender) Send(to, subject, htmlBody string) error { payload := map[string]interface{}{ "from": s.fromAddr, "to": []string{to}, "subject": subject, "html": htmlBody, } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("email: marshal payload: %w", err) } req, err := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewReader(body)) if err != nil { return fmt.Errorf("email: create request: %w", err) } req.Header.Set("Authorization", "Bearer "+s.apiKey) req.Header.Set("Content-Type", "application/json") resp, err := s.client.Do(req) if err != nil { return fmt.Errorf("email: send request: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { respBody, _ := io.ReadAll(resp.Body) slog.Error("email: resend API error", "status", resp.StatusCode, "body", string(respBody), "to", to, "subject", subject) return fmt.Errorf("email: resend API returned %d: %s", resp.StatusCode, string(respBody)) } slog.Info("email: sent via Resend", "to", to, "subject", subject, "status", resp.StatusCode) return nil } // Template IDs for standard emails. const ( TemplateWelcome = "welcome" TemplatePasswordReset = "password_reset" TemplateIncidentAlert = "incident_alert" TemplatePlanUpgrade = "plan_upgrade" TemplateInvoice = "invoice" ) // Service provides email notifications with templates. type Service struct { sender Sender fromName string fromAddr string } // NewService creates an email service. // Pass nil sender for stub mode (logs only). // For Resend: NewService(NewResendSender(apiKey, from), "SYNTREX", "noreply@syntrex.pro") func NewService(sender Sender, fromName, fromAddr string) *Service { if sender == nil { sender = &StubSender{} } if fromName == "" { fromName = "SYNTREX" } if fromAddr == "" { fromAddr = "noreply@syntrex.pro" } return &Service{ sender: sender, fromName: fromName, fromAddr: fromAddr, } } // SendVerificationCode sends a 6-digit verification code after registration. // locale: "ru" for Russian, anything else for English. func (s *Service) SendVerificationCode(toEmail, userName, code string) error { return s.SendVerificationCodeLocalized(toEmail, userName, code, "ru") } // SendVerificationCodeLocalized sends a localized verification code email. func (s *Service) SendVerificationCodeLocalized(toEmail, userName, code, locale string) error { ru := locale == "ru" var subject, greeting, instruction, validity, disclaimer string if ru { subject = "SYNTREX — Код подтверждения" greeting = fmt.Sprintf("Здравствуйте, %s!", userName) instruction = "Ваш код подтверждения email:" validity = "Код действителен 24 часа." disclaimer = "Если вы не регистрировались на SYNTREX — проигнорируйте это письмо." } else { subject = "SYNTREX — Verification Code" greeting = fmt.Sprintf("Hello, %s!", userName) instruction = "Your email verification code:" validity = "This code is valid for 24 hours." disclaimer = "If you did not sign up for SYNTREX, please ignore this email." } body := fmt.Sprintf(`

🛡️ SYNTREX

%s

%s

%s

%s

%s

`, greeting, instruction, code, validity, disclaimer) return s.sender.Send(toEmail, subject, body) } // SendWelcome sends a welcome email after registration. func (s *Service) SendWelcome(toEmail, userName, orgName string) error { subject := "Welcome to SYNTREX / Добро пожаловать в SYNTREX" body := fmt.Sprintf(`

🛡️ SYNTREX

Hello / Здравствуйте, %s!

Your organization %s has been registered. / Ваша организация %s успешно зарегистрирована.

Getting Started / Как начать:

  1. Open Quick Start in the sidebar / Откройте Quick Start в боковом меню
  2. Create an API key in Settings → API Keys / Создайте API-ключ
  3. Send your first security event / Отправьте первое событие

This is an automated email from SYNTREX. If you did not register, please ignore it.

`, userName, orgName, orgName) return s.sender.Send(toEmail, subject, body) } // SendIncidentAlert sends an alert when a critical incident is created. func (s *Service) SendIncidentAlert(toEmail, incidentID, title, severity string) error { subject := fmt.Sprintf("[SYNTREX] Инцидент %s: %s", severity, title) body := fmt.Sprintf(`

🚨 Инцидент безопасности

ID:%s
Название:%s
Критичность:%s
`, incidentID, title, severity) return s.sender.Send(toEmail, subject, body) } // SendPasswordReset sends a password reset link. func (s *Service) SendPasswordReset(toEmail, resetToken string) error { subject := "SYNTREX — Сброс пароля" body := fmt.Sprintf(`

🔐 Сброс пароля

Вы запросили сброс пароля. Нажмите кнопку ниже:

Сбросить пароль

Ссылка действительна 1 час. Если вы не запрашивали сброс — проигнорируйте.

`, resetToken) return s.sender.Send(toEmail, subject, body) }