Files
breakpilot-core/consent-service/internal/services/email_service.go
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

251 lines
7.3 KiB
Go

package services
import (
"bytes"
"fmt"
"net/smtp"
"strings"
)
// EmailConfig holds SMTP configuration
type EmailConfig struct {
Host string
Port int
Username string
Password string
FromName string
FromAddr string
BaseURL string // Frontend URL for links
}
// EmailService handles sending emails
type EmailService struct {
config EmailConfig
}
// NewEmailService creates a new EmailService
func NewEmailService(config EmailConfig) *EmailService {
return &EmailService{config: config}
}
// SendEmail sends an email
func (s *EmailService) SendEmail(to, subject, htmlBody, textBody string) error {
// Build MIME message
var msg bytes.Buffer
msg.WriteString(fmt.Sprintf("From: %s <%s>\r\n", s.config.FromName, s.config.FromAddr))
msg.WriteString(fmt.Sprintf("To: %s\r\n", to))
msg.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
msg.WriteString("MIME-Version: 1.0\r\n")
msg.WriteString("Content-Type: multipart/alternative; boundary=\"boundary42\"\r\n")
msg.WriteString("\r\n")
// Text part
msg.WriteString("--boundary42\r\n")
msg.WriteString("Content-Type: text/plain; charset=\"UTF-8\"\r\n")
msg.WriteString("\r\n")
msg.WriteString(textBody)
msg.WriteString("\r\n")
// HTML part
msg.WriteString("--boundary42\r\n")
msg.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n")
msg.WriteString("\r\n")
msg.WriteString(htmlBody)
msg.WriteString("\r\n")
msg.WriteString("--boundary42--\r\n")
// Send email
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host)
err := smtp.SendMail(addr, auth, s.config.FromAddr, []string{to}, msg.Bytes())
if err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
// SendVerificationEmail sends an email verification email
func (s *EmailService) SendVerificationEmail(to, name, token string) error {
verifyLink := fmt.Sprintf("%s/verify-email?token=%s", s.config.BaseURL, token)
subject := "Bitte bestätigen Sie Ihre E-Mail-Adresse - BreakPilot"
textBody := fmt.Sprintf(`Hallo %s,
Willkommen bei BreakPilot!
Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie den folgenden Link öffnen:
%s
Dieser Link ist 24 Stunden gültig.
Falls Sie sich nicht bei BreakPilot registriert haben, können Sie diese E-Mail ignorieren.
Mit freundlichen Grüßen,
Ihr BreakPilot Team`, getDisplayName(name), verifyLink)
htmlBody := s.renderTemplate("verification", map[string]interface{}{
"Name": getDisplayName(name),
"VerifyLink": verifyLink,
})
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendPasswordResetEmail sends a password reset email
func (s *EmailService) SendPasswordResetEmail(to, name, token string) error {
resetLink := fmt.Sprintf("%s/reset-password?token=%s", s.config.BaseURL, token)
subject := "Passwort zurücksetzen - BreakPilot"
textBody := fmt.Sprintf(`Hallo %s,
Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.
Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:
%s
Dieser Link ist 1 Stunde gültig.
Falls Sie keine Passwort-Zurücksetzung angefordert haben, können Sie diese E-Mail ignorieren.
Mit freundlichen Grüßen,
Ihr BreakPilot Team`, getDisplayName(name), resetLink)
htmlBody := s.renderTemplate("password_reset", map[string]interface{}{
"Name": getDisplayName(name),
"ResetLink": resetLink,
})
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendNewVersionNotification sends a notification about new document version
func (s *EmailService) SendNewVersionNotification(to, name, documentName, documentType string, deadlineDays int) error {
consentLink := fmt.Sprintf("%s/app?consent=pending", s.config.BaseURL)
subject := fmt.Sprintf("Neue Version: %s - Bitte bestätigen Sie innerhalb von %d Tagen", documentName, deadlineDays)
textBody := fmt.Sprintf(`Hallo %s,
Wir haben unsere %s aktualisiert.
Bitte lesen und bestätigen Sie die neuen Bedingungen innerhalb der nächsten %d Tage:
%s
Falls Sie nicht innerhalb dieser Frist bestätigen, wird Ihr Account vorübergehend gesperrt.
Mit freundlichen Grüßen,
Ihr BreakPilot Team`, getDisplayName(name), documentName, deadlineDays, consentLink)
htmlBody := s.renderTemplate("new_version", map[string]interface{}{
"Name": getDisplayName(name),
"DocumentName": documentName,
"DeadlineDays": deadlineDays,
"ConsentLink": consentLink,
})
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendConsentReminder sends a consent reminder email
func (s *EmailService) SendConsentReminder(to, name string, documents []string, daysLeft int) error {
consentLink := fmt.Sprintf("%s/app?consent=pending", s.config.BaseURL)
urgency := "Erinnerung"
if daysLeft <= 7 {
urgency = "Dringend"
}
if daysLeft <= 2 {
urgency = "Letzte Warnung"
}
subject := fmt.Sprintf("%s: Noch %d Tage um ausstehende Dokumente zu bestätigen", urgency, daysLeft)
docList := strings.Join(documents, "\n- ")
textBody := fmt.Sprintf(`Hallo %s,
Dies ist eine freundliche Erinnerung, dass Sie noch ausstehende rechtliche Dokumente bestätigen müssen.
Ausstehende Dokumente:
- %s
Sie haben noch %d Tage Zeit. Nach Ablauf dieser Frist wird Ihr Account vorübergehend gesperrt.
Bitte bestätigen Sie hier:
%s
Mit freundlichen Grüßen,
Ihr BreakPilot Team`, getDisplayName(name), docList, daysLeft, consentLink)
htmlBody := s.renderTemplate("reminder", map[string]interface{}{
"Name": getDisplayName(name),
"Documents": documents,
"DaysLeft": daysLeft,
"Urgency": urgency,
"ConsentLink": consentLink,
})
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendAccountSuspendedNotification sends notification when account is suspended
func (s *EmailService) SendAccountSuspendedNotification(to, name string, documents []string) error {
consentLink := fmt.Sprintf("%s/app?consent=pending", s.config.BaseURL)
subject := "Ihr Account wurde vorübergehend gesperrt - BreakPilot"
docList := strings.Join(documents, "\n- ")
textBody := fmt.Sprintf(`Hallo %s,
Ihr Account wurde vorübergehend gesperrt, da Sie die folgenden rechtlichen Dokumente nicht innerhalb der Frist bestätigt haben:
- %s
Um Ihren Account zu entsperren, bestätigen Sie bitte alle ausstehenden Dokumente:
%s
Sobald Sie alle Dokumente bestätigt haben, wird Ihr Account automatisch entsperrt.
Mit freundlichen Grüßen,
Ihr BreakPilot Team`, getDisplayName(name), docList, consentLink)
htmlBody := s.renderTemplate("suspended", map[string]interface{}{
"Name": getDisplayName(name),
"Documents": documents,
"ConsentLink": consentLink,
})
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendAccountReactivatedNotification sends notification when account is reactivated
func (s *EmailService) SendAccountReactivatedNotification(to, name string) error {
appLink := fmt.Sprintf("%s/app", s.config.BaseURL)
subject := "Ihr Account wurde wieder aktiviert - BreakPilot"
textBody := fmt.Sprintf(`Hallo %s,
Vielen Dank für die Bestätigung der rechtlichen Dokumente!
Ihr Account wurde wieder aktiviert und Sie können BreakPilot wie gewohnt nutzen:
%s
Mit freundlichen Grüßen,
Ihr BreakPilot Team`, getDisplayName(name), appLink)
htmlBody := s.renderTemplate("reactivated", map[string]interface{}{
"Name": getDisplayName(name),
"AppLink": appLink,
})
return s.SendEmail(to, subject, htmlBody, textBody)
}