// 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@отражение.рус") func NewService(sender Sender, fromName, fromAddr string) *Service { if sender == nil { sender = &StubSender{} } if fromName == "" { fromName = "SYNTREX" } if fromAddr == "" { fromAddr = "noreply@xn--80akacl3adqr.xn--p1acf" } return &Service{ sender: sender, fromName: fromName, fromAddr: fromAddr, } } // SendVerificationCode sends a 6-digit verification code after registration. func (s *Service) SendVerificationCode(toEmail, userName, code string) error { subject := "SYNTREX — Код подтверждения" body := fmt.Sprintf(`

🛡️ SYNTREX

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

Ваш код подтверждения email:

%s

Код действителен 24 часа.

Если вы не регистрировались на SYNTREX — проигнорируйте это письмо.

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

🛡️ SYNTREX

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

Ваша организация %s успешно зарегистрирована.

Как начать:

  1. Откройте Quick Start в боковом меню
  2. Создайте API-ключ в Настройки → API Keys
  3. Отправьте первое событие безопасности

Это автоматическое письмо от SYNTREX. Если вы не регистрировались — проигнорируйте.

`, userName, 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) }