Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
555 lines
20 KiB
Go
555 lines
20 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"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)
|
|
}
|
|
|
|
// renderTemplate renders an email HTML template
|
|
func (s *EmailService) renderTemplate(templateName string, data map[string]interface{}) string {
|
|
templates := map[string]string{
|
|
"verification": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Willkommen bei BreakPilot!</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Hallo {{.Name}},</p>
|
|
<p>Vielen Dank für Ihre Registrierung! Bitte bestätigen Sie Ihre E-Mail-Adresse, um Ihr Konto zu aktivieren.</p>
|
|
<p style="text-align: center;">
|
|
<a href="{{.VerifyLink}}" class="button">E-Mail bestätigen</a>
|
|
</p>
|
|
<p>Dieser Link ist 24 Stunden gültig.</p>
|
|
<p>Falls Sie sich nicht bei BreakPilot registriert haben, können Sie diese E-Mail ignorieren.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
|
|
"password_reset": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Passwort zurücksetzen</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Hallo {{.Name}},</p>
|
|
<p>Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.</p>
|
|
<p style="text-align: center;">
|
|
<a href="{{.ResetLink}}" class="button">Passwort zurücksetzen</a>
|
|
</p>
|
|
<div class="warning">
|
|
<strong>Hinweis:</strong> Dieser Link ist nur 1 Stunde gültig.
|
|
</div>
|
|
<p>Falls Sie keine Passwort-Zurücksetzung angefordert haben, können Sie diese E-Mail ignorieren.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
|
|
"new_version": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.info-box { background: #e0e7ff; border-left: 4px solid #6366f1; padding: 12px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Neue Version: {{.DocumentName}}</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Hallo {{.Name}},</p>
|
|
<p>Wir haben unsere <strong>{{.DocumentName}}</strong> aktualisiert.</p>
|
|
<div class="info-box">
|
|
<strong>Wichtig:</strong> Bitte bestätigen Sie die neuen Bedingungen innerhalb der nächsten <strong>{{.DeadlineDays}} Tage</strong>.
|
|
</div>
|
|
<p style="text-align: center;">
|
|
<a href="{{.ConsentLink}}" class="button">Dokument ansehen & bestätigen</a>
|
|
</p>
|
|
<p>Falls Sie nicht innerhalb dieser Frist bestätigen, wird Ihr Account vorübergehend gesperrt.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
|
|
"reminder": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #f59e0b, #d97706); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #f59e0b; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px; margin: 20px 0; }
|
|
.doc-list { background: white; padding: 15px; border-radius: 8px; margin: 15px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>{{.Urgency}}: Ausstehende Bestätigungen</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Hallo {{.Name}},</p>
|
|
<p>Dies ist eine freundliche Erinnerung, dass Sie noch ausstehende rechtliche Dokumente bestätigen müssen.</p>
|
|
<div class="doc-list">
|
|
<strong>Ausstehende Dokumente:</strong>
|
|
<ul>
|
|
{{range .Documents}}<li>{{.}}</li>{{end}}
|
|
</ul>
|
|
</div>
|
|
<div class="warning">
|
|
<strong>Sie haben noch {{.DaysLeft}} Tage Zeit.</strong> Nach Ablauf dieser Frist wird Ihr Account vorübergehend gesperrt.
|
|
</div>
|
|
<p style="text-align: center;">
|
|
<a href="{{.ConsentLink}}" class="button">Jetzt bestätigen</a>
|
|
</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
|
|
"suspended": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.alert { background: #fee2e2; border-left: 4px solid #ef4444; padding: 12px; margin: 20px 0; }
|
|
.doc-list { background: white; padding: 15px; border-radius: 8px; margin: 15px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Account vorübergehend gesperrt</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Hallo {{.Name}},</p>
|
|
<div class="alert">
|
|
<strong>Ihr Account wurde vorübergehend gesperrt</strong>, da Sie die folgenden rechtlichen Dokumente nicht innerhalb der Frist bestätigt haben.
|
|
</div>
|
|
<div class="doc-list">
|
|
<strong>Nicht bestätigte Dokumente:</strong>
|
|
<ul>
|
|
{{range .Documents}}<li>{{.}}</li>{{end}}
|
|
</ul>
|
|
</div>
|
|
<p>Um Ihren Account zu entsperren, bestätigen Sie bitte alle ausstehenden Dokumente:</p>
|
|
<p style="text-align: center;">
|
|
<a href="{{.ConsentLink}}" class="button">Dokumente bestätigen & Account entsperren</a>
|
|
</p>
|
|
<p>Sobald Sie alle Dokumente bestätigt haben, wird Ihr Account automatisch entsperrt.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
|
|
"reactivated": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #22c55e, #16a34a); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #22c55e; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.success { background: #dcfce7; border-left: 4px solid #22c55e; padding: 12px; margin: 20px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Account wieder aktiviert!</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Hallo {{.Name}},</p>
|
|
<div class="success">
|
|
<strong>Vielen Dank!</strong> Ihr Account wurde erfolgreich wieder aktiviert.
|
|
</div>
|
|
<p>Sie können BreakPilot ab sofort wieder wie gewohnt nutzen.</p>
|
|
<p style="text-align: center;">
|
|
<a href="{{.AppLink}}" class="button">Zu BreakPilot</a>
|
|
</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
|
|
"generic_notification": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; padding: 30px; border-radius: 10px 10px 0 0; text-align: center; }
|
|
.content { background: #f8fafc; padding: 30px; border-radius: 0 0 10px 10px; }
|
|
.button { display: inline-block; background: #6366f1; color: white; padding: 14px 28px; text-decoration: none; border-radius: 8px; font-weight: 600; margin: 20px 0; }
|
|
.footer { text-align: center; color: #64748b; font-size: 12px; margin-top: 30px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>{{.Title}}</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>{{.Body}}</p>
|
|
<p style="text-align: center;">
|
|
<a href="{{.BaseURL}}/app" class="button">Zu BreakPilot</a>
|
|
</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>© 2024 BreakPilot. Alle Rechte vorbehalten.</p>
|
|
</div>
|
|
</body>
|
|
</html>`,
|
|
}
|
|
|
|
tmplStr, ok := templates[templateName]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
tmpl, err := template.New(templateName).Parse(tmplStr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return ""
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// SendConsentReminderEmail sends a simplified consent reminder email
|
|
func (s *EmailService) SendConsentReminderEmail(to, title, body string) error {
|
|
subject := title
|
|
|
|
htmlBody := s.renderTemplate("generic_notification", map[string]interface{}{
|
|
"Title": title,
|
|
"Body": body,
|
|
"BaseURL": s.config.BaseURL,
|
|
})
|
|
|
|
return s.SendEmail(to, subject, htmlBody, body)
|
|
}
|
|
|
|
// SendGenericNotificationEmail sends a generic notification email
|
|
func (s *EmailService) SendGenericNotificationEmail(to, title, body string) error {
|
|
subject := title
|
|
|
|
htmlBody := s.renderTemplate("generic_notification", map[string]interface{}{
|
|
"Title": title,
|
|
"Body": body,
|
|
"BaseURL": s.config.BaseURL,
|
|
})
|
|
|
|
return s.SendEmail(to, subject, htmlBody, body)
|
|
}
|
|
|
|
// getDisplayName returns display name or fallback
|
|
func getDisplayName(name string) string {
|
|
if name != "" {
|
|
return name
|
|
}
|
|
return "Nutzer"
|
|
}
|