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>
251 lines
7.3 KiB
Go
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)
|
|
}
|
|
|