package services import ( "context" "encoding/json" "fmt" "regexp" "strings" "time" "github.com/breakpilot/consent-service/internal/models" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // EmailTemplateService handles email template management type EmailTemplateService struct { db *pgxpool.Pool } // NewEmailTemplateService creates a new email template service func NewEmailTemplateService(db *pgxpool.Pool) *EmailTemplateService { return &EmailTemplateService{db: db} } // GetAllTemplateTypes returns all available email template types with their variables func (s *EmailTemplateService) GetAllTemplateTypes() []models.EmailTemplateVariables { return []models.EmailTemplateVariables{ { TemplateType: models.EmailTypeWelcome, Variables: []string{"user_name", "user_email", "login_url", "support_email"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "user_email": "E-Mail-Adresse des Benutzers", "login_url": "URL zur Login-Seite", "support_email": "Support E-Mail-Adresse", }, }, { TemplateType: models.EmailTypeEmailVerification, Variables: []string{"user_name", "verification_url", "verification_code", "expires_in"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "verification_url": "URL zur E-Mail-Verifizierung", "verification_code": "Verifizierungscode", "expires_in": "Gültigkeit des Links (z.B. '24 Stunden')", }, }, { TemplateType: models.EmailTypePasswordReset, Variables: []string{"user_name", "reset_url", "reset_code", "expires_in", "ip_address"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "reset_url": "URL zum Passwort-Reset", "reset_code": "Reset-Code", "expires_in": "Gültigkeit des Links", "ip_address": "IP-Adresse der Anfrage", }, }, { TemplateType: models.EmailTypePasswordChanged, Variables: []string{"user_name", "changed_at", "ip_address", "device_info", "support_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "changed_at": "Zeitpunkt der Änderung", "ip_address": "IP-Adresse", "device_info": "Geräte-Informationen", "support_url": "URL zum Support", }, }, { TemplateType: models.EmailType2FAEnabled, Variables: []string{"user_name", "enabled_at", "device_info", "security_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "enabled_at": "Zeitpunkt der Aktivierung", "device_info": "Geräte-Informationen", "security_url": "URL zu Sicherheitseinstellungen", }, }, { TemplateType: models.EmailType2FADisabled, Variables: []string{"user_name", "disabled_at", "ip_address", "security_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "disabled_at": "Zeitpunkt der Deaktivierung", "ip_address": "IP-Adresse", "security_url": "URL zu Sicherheitseinstellungen", }, }, { TemplateType: models.EmailTypeNewDeviceLogin, Variables: []string{"user_name", "login_time", "ip_address", "device_info", "location", "security_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "login_time": "Zeitpunkt des Logins", "ip_address": "IP-Adresse", "device_info": "Geräte-Informationen", "location": "Ungefährer Standort", "security_url": "URL zu Sicherheitseinstellungen", }, }, { TemplateType: models.EmailTypeSuspiciousActivity, Variables: []string{"user_name", "activity_type", "activity_time", "ip_address", "security_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "activity_type": "Art der Aktivität", "activity_time": "Zeitpunkt", "ip_address": "IP-Adresse", "security_url": "URL zu Sicherheitseinstellungen", }, }, { TemplateType: models.EmailTypeAccountLocked, Variables: []string{"user_name", "locked_at", "reason", "unlock_time", "support_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "locked_at": "Zeitpunkt der Sperrung", "reason": "Grund der Sperrung", "unlock_time": "Zeitpunkt der automatischen Entsperrung", "support_url": "URL zum Support", }, }, { TemplateType: models.EmailTypeAccountUnlocked, Variables: []string{"user_name", "unlocked_at", "login_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "unlocked_at": "Zeitpunkt der Entsperrung", "login_url": "URL zur Login-Seite", }, }, { TemplateType: models.EmailTypeDeletionRequested, Variables: []string{"user_name", "requested_at", "deletion_date", "cancel_url", "data_info"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "requested_at": "Zeitpunkt der Anfrage", "deletion_date": "Datum der endgültigen Löschung", "cancel_url": "URL zum Abbrechen", "data_info": "Info über zu löschende Daten", }, }, { TemplateType: models.EmailTypeDeletionConfirmed, Variables: []string{"user_name", "deleted_at", "feedback_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "deleted_at": "Zeitpunkt der Löschung", "feedback_url": "URL für Feedback", }, }, { TemplateType: models.EmailTypeDataExportReady, Variables: []string{"user_name", "download_url", "expires_in", "file_size"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "download_url": "URL zum Download", "expires_in": "Gültigkeit des Download-Links", "file_size": "Dateigröße", }, }, { TemplateType: models.EmailTypeEmailChanged, Variables: []string{"user_name", "old_email", "new_email", "changed_at", "support_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "old_email": "Alte E-Mail-Adresse", "new_email": "Neue E-Mail-Adresse", "changed_at": "Zeitpunkt der Änderung", "support_url": "URL zum Support", }, }, { TemplateType: models.EmailTypeEmailChangeVerify, Variables: []string{"user_name", "new_email", "verification_url", "expires_in"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "new_email": "Neue E-Mail-Adresse", "verification_url": "URL zur Verifizierung", "expires_in": "Gültigkeit des Links", }, }, { TemplateType: models.EmailTypeNewVersionPublished, Variables: []string{"user_name", "document_name", "document_type", "version", "consent_url", "deadline"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "document_name": "Name des Dokuments", "document_type": "Typ des Dokuments", "version": "Versionsnummer", "consent_url": "URL zur Zustimmung", "deadline": "Frist für die Zustimmung", }, }, { TemplateType: models.EmailTypeConsentReminder, Variables: []string{"user_name", "document_name", "days_left", "consent_url", "deadline"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "document_name": "Name des Dokuments", "days_left": "Verbleibende Tage", "consent_url": "URL zur Zustimmung", "deadline": "Frist für die Zustimmung", }, }, { TemplateType: models.EmailTypeConsentDeadlineWarning, Variables: []string{"user_name", "document_name", "hours_left", "consent_url", "consequences"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "document_name": "Name des Dokuments", "hours_left": "Verbleibende Stunden", "consent_url": "URL zur Zustimmung", "consequences": "Konsequenzen bei Nicht-Zustimmung", }, }, { TemplateType: models.EmailTypeAccountSuspended, Variables: []string{"user_name", "suspended_at", "reason", "documents", "consent_url"}, Descriptions: map[string]string{ "user_name": "Name des Benutzers", "suspended_at": "Zeitpunkt der Suspendierung", "reason": "Grund der Suspendierung", "documents": "Liste der fehlenden Zustimmungen", "consent_url": "URL zur Zustimmung", }, }, } } // CreateEmailTemplate creates a new email template type func (s *EmailTemplateService) CreateEmailTemplate(ctx context.Context, req *models.CreateEmailTemplateRequest) (*models.EmailTemplate, error) { template := &models.EmailTemplate{ ID: uuid.New(), Type: req.Type, Name: req.Name, Description: req.Description, IsActive: true, SortOrder: 0, CreatedAt: time.Now(), UpdatedAt: time.Now(), } _, err := s.db.Exec(ctx, ` INSERT INTO email_templates (id, type, name, description, is_active, sort_order, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) `, template.ID, template.Type, template.Name, template.Description, template.IsActive, template.SortOrder, template.CreatedAt, template.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to create email template: %w", err) } return template, nil } // GetAllTemplates returns all email templates with their latest published versions func (s *EmailTemplateService) GetAllTemplates(ctx context.Context) ([]models.EmailTemplateWithVersion, error) { rows, err := s.db.Query(ctx, ` SELECT t.id, t.type, t.name, t.description, t.is_active, t.sort_order, t.created_at, t.updated_at, v.id, v.template_id, v.version, v.language, v.subject, v.body_html, v.body_text, v.summary, v.status, v.published_at, v.scheduled_publish_at, v.created_by, v.approved_by, v.approved_at, v.created_at, v.updated_at FROM email_templates t LEFT JOIN email_template_versions v ON t.id = v.template_id AND v.status = 'published' ORDER BY t.sort_order, t.name `) if err != nil { return nil, fmt.Errorf("failed to get email templates: %w", err) } defer rows.Close() var results []models.EmailTemplateWithVersion for rows.Next() { var template models.EmailTemplate var versionID, templateID, createdBy, approvedBy *uuid.UUID var publishedAt, scheduledPublishAt, approvedAt, vCreatedAt, vUpdatedAt *time.Time var vVersion, vLanguage, vSubject, vBodyHTML, vBodyText, vSummary, vStatus *string err := rows.Scan( &template.ID, &template.Type, &template.Name, &template.Description, &template.IsActive, &template.SortOrder, &template.CreatedAt, &template.UpdatedAt, &versionID, &templateID, &vVersion, &vLanguage, &vSubject, &vBodyHTML, &vBodyText, &vSummary, &vStatus, &publishedAt, &scheduledPublishAt, &createdBy, &approvedBy, &approvedAt, &vCreatedAt, &vUpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan email template: %w", err) } result := models.EmailTemplateWithVersion{Template: template} if versionID != nil { result.LatestVersion = &models.EmailTemplateVersion{ ID: *versionID, TemplateID: *templateID, Version: *vVersion, Language: *vLanguage, Subject: *vSubject, BodyHTML: *vBodyHTML, BodyText: *vBodyText, Status: *vStatus, PublishedAt: publishedAt, ScheduledPublishAt: scheduledPublishAt, CreatedBy: createdBy, ApprovedBy: approvedBy, ApprovedAt: approvedAt, } if vSummary != nil { result.LatestVersion.Summary = vSummary } if vCreatedAt != nil { result.LatestVersion.CreatedAt = *vCreatedAt } if vUpdatedAt != nil { result.LatestVersion.UpdatedAt = *vUpdatedAt } } results = append(results, result) } return results, nil } // GetTemplateByID returns a template by ID func (s *EmailTemplateService) GetTemplateByID(ctx context.Context, id uuid.UUID) (*models.EmailTemplate, error) { var template models.EmailTemplate err := s.db.QueryRow(ctx, ` SELECT id, type, name, description, is_active, sort_order, created_at, updated_at FROM email_templates WHERE id = $1 `, id).Scan(&template.ID, &template.Type, &template.Name, &template.Description, &template.IsActive, &template.SortOrder, &template.CreatedAt, &template.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to get email template: %w", err) } return &template, nil } // GetTemplateByType returns a template by type func (s *EmailTemplateService) GetTemplateByType(ctx context.Context, templateType string) (*models.EmailTemplate, error) { var template models.EmailTemplate err := s.db.QueryRow(ctx, ` SELECT id, type, name, description, is_active, sort_order, created_at, updated_at FROM email_templates WHERE type = $1 `, templateType).Scan(&template.ID, &template.Type, &template.Name, &template.Description, &template.IsActive, &template.SortOrder, &template.CreatedAt, &template.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to get email template: %w", err) } return &template, nil } // CreateTemplateVersion creates a new version of an email template func (s *EmailTemplateService) CreateTemplateVersion(ctx context.Context, req *models.CreateEmailTemplateVersionRequest, createdBy uuid.UUID) (*models.EmailTemplateVersion, error) { templateID, err := uuid.Parse(req.TemplateID) if err != nil { return nil, fmt.Errorf("invalid template ID: %w", err) } version := &models.EmailTemplateVersion{ ID: uuid.New(), TemplateID: templateID, Version: req.Version, Language: req.Language, Subject: req.Subject, BodyHTML: req.BodyHTML, BodyText: req.BodyText, Summary: req.Summary, Status: "draft", CreatedBy: &createdBy, CreatedAt: time.Now(), UpdatedAt: time.Now(), } _, err = s.db.Exec(ctx, ` INSERT INTO email_template_versions (id, template_id, version, language, subject, body_html, body_text, summary, status, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) `, version.ID, version.TemplateID, version.Version, version.Language, version.Subject, version.BodyHTML, version.BodyText, version.Summary, version.Status, version.CreatedBy, version.CreatedAt, version.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to create email template version: %w", err) } return version, nil } // GetVersionsByTemplateID returns all versions for a template func (s *EmailTemplateService) GetVersionsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]models.EmailTemplateVersion, error) { rows, err := s.db.Query(ctx, ` SELECT id, template_id, version, language, subject, body_html, body_text, summary, status, published_at, scheduled_publish_at, created_by, approved_by, approved_at, created_at, updated_at FROM email_template_versions WHERE template_id = $1 ORDER BY created_at DESC `, templateID) if err != nil { return nil, fmt.Errorf("failed to get template versions: %w", err) } defer rows.Close() var versions []models.EmailTemplateVersion for rows.Next() { var v models.EmailTemplateVersion err := rows.Scan(&v.ID, &v.TemplateID, &v.Version, &v.Language, &v.Subject, &v.BodyHTML, &v.BodyText, &v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy, &v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to scan template version: %w", err) } versions = append(versions, v) } return versions, nil } // GetVersionByID returns a version by ID func (s *EmailTemplateService) GetVersionByID(ctx context.Context, id uuid.UUID) (*models.EmailTemplateVersion, error) { var v models.EmailTemplateVersion err := s.db.QueryRow(ctx, ` SELECT id, template_id, version, language, subject, body_html, body_text, summary, status, published_at, scheduled_publish_at, created_by, approved_by, approved_at, created_at, updated_at FROM email_template_versions WHERE id = $1 `, id).Scan(&v.ID, &v.TemplateID, &v.Version, &v.Language, &v.Subject, &v.BodyHTML, &v.BodyText, &v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy, &v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to get template version: %w", err) } return &v, nil } // GetPublishedVersion returns the published version for a template and language func (s *EmailTemplateService) GetPublishedVersion(ctx context.Context, templateType, language string) (*models.EmailTemplateVersion, error) { var v models.EmailTemplateVersion err := s.db.QueryRow(ctx, ` SELECT v.id, v.template_id, v.version, v.language, v.subject, v.body_html, v.body_text, v.summary, v.status, v.published_at, v.scheduled_publish_at, v.created_by, v.approved_by, v.approved_at, v.created_at, v.updated_at FROM email_template_versions v JOIN email_templates t ON t.id = v.template_id WHERE t.type = $1 AND v.language = $2 AND v.status = 'published' ORDER BY v.published_at DESC LIMIT 1 `, templateType, language).Scan(&v.ID, &v.TemplateID, &v.Version, &v.Language, &v.Subject, &v.BodyHTML, &v.BodyText, &v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy, &v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt) if err != nil { return nil, fmt.Errorf("failed to get published version: %w", err) } return &v, nil } // UpdateVersion updates a version func (s *EmailTemplateService) UpdateVersion(ctx context.Context, id uuid.UUID, req *models.UpdateEmailTemplateVersionRequest) error { query := "UPDATE email_template_versions SET updated_at = $1" args := []interface{}{time.Now()} argIdx := 2 if req.Subject != nil { query += fmt.Sprintf(", subject = $%d", argIdx) args = append(args, *req.Subject) argIdx++ } if req.BodyHTML != nil { query += fmt.Sprintf(", body_html = $%d", argIdx) args = append(args, *req.BodyHTML) argIdx++ } if req.BodyText != nil { query += fmt.Sprintf(", body_text = $%d", argIdx) args = append(args, *req.BodyText) argIdx++ } if req.Summary != nil { query += fmt.Sprintf(", summary = $%d", argIdx) args = append(args, *req.Summary) argIdx++ } if req.Status != nil { query += fmt.Sprintf(", status = $%d", argIdx) args = append(args, *req.Status) argIdx++ } query += fmt.Sprintf(" WHERE id = $%d", argIdx) args = append(args, id) _, err := s.db.Exec(ctx, query, args...) if err != nil { return fmt.Errorf("failed to update version: %w", err) } return nil } // SubmitForReview submits a version for review func (s *EmailTemplateService) SubmitForReview(ctx context.Context, versionID, submitterID uuid.UUID, comment *string) error { tx, err := s.db.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) // Update status _, err = tx.Exec(ctx, ` UPDATE email_template_versions SET status = 'review', updated_at = $1 WHERE id = $2 `, time.Now(), versionID) if err != nil { return fmt.Errorf("failed to update status: %w", err) } // Create approval record _, err = tx.Exec(ctx, ` INSERT INTO email_template_approvals (id, version_id, approver_id, action, comment, created_at) VALUES ($1, $2, $3, $4, $5, $6) `, uuid.New(), versionID, submitterID, "submitted_for_review", comment, time.Now()) if err != nil { return fmt.Errorf("failed to create approval record: %w", err) } return tx.Commit(ctx) } // ApproveVersion approves a version (DSB) func (s *EmailTemplateService) ApproveVersion(ctx context.Context, versionID, approverID uuid.UUID, comment *string, scheduledPublishAt *time.Time) error { tx, err := s.db.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) now := time.Now() status := "approved" if scheduledPublishAt != nil { status = "scheduled" } _, err = tx.Exec(ctx, ` UPDATE email_template_versions SET status = $1, approved_by = $2, approved_at = $3, scheduled_publish_at = $4, updated_at = $5 WHERE id = $6 `, status, approverID, now, scheduledPublishAt, now, versionID) if err != nil { return fmt.Errorf("failed to approve version: %w", err) } _, err = tx.Exec(ctx, ` INSERT INTO email_template_approvals (id, version_id, approver_id, action, comment, created_at) VALUES ($1, $2, $3, $4, $5, $6) `, uuid.New(), versionID, approverID, "approved", comment, now) if err != nil { return fmt.Errorf("failed to create approval record: %w", err) } return tx.Commit(ctx) } // PublishVersion publishes an approved version func (s *EmailTemplateService) PublishVersion(ctx context.Context, versionID, publisherID uuid.UUID) error { tx, err := s.db.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) // Get version info to find template and language var templateID uuid.UUID var language string err = tx.QueryRow(ctx, `SELECT template_id, language FROM email_template_versions WHERE id = $1`, versionID).Scan(&templateID, &language) if err != nil { return fmt.Errorf("failed to get version info: %w", err) } // Archive old published versions for same template and language _, err = tx.Exec(ctx, ` UPDATE email_template_versions SET status = 'archived', updated_at = $1 WHERE template_id = $2 AND language = $3 AND status = 'published' `, time.Now(), templateID, language) if err != nil { return fmt.Errorf("failed to archive old versions: %w", err) } // Publish the new version now := time.Now() _, err = tx.Exec(ctx, ` UPDATE email_template_versions SET status = 'published', published_at = $1, updated_at = $2 WHERE id = $3 `, now, now, versionID) if err != nil { return fmt.Errorf("failed to publish version: %w", err) } // Create approval record _, err = tx.Exec(ctx, ` INSERT INTO email_template_approvals (id, version_id, approver_id, action, created_at) VALUES ($1, $2, $3, $4, $5) `, uuid.New(), versionID, publisherID, "published", now) if err != nil { return fmt.Errorf("failed to create approval record: %w", err) } return tx.Commit(ctx) } // RejectVersion rejects a version func (s *EmailTemplateService) RejectVersion(ctx context.Context, versionID, rejectorID uuid.UUID, comment string) error { tx, err := s.db.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) now := time.Now() _, err = tx.Exec(ctx, ` UPDATE email_template_versions SET status = 'draft', updated_at = $1 WHERE id = $2 `, now, versionID) if err != nil { return fmt.Errorf("failed to reject version: %w", err) } _, err = tx.Exec(ctx, ` INSERT INTO email_template_approvals (id, version_id, approver_id, action, comment, created_at) VALUES ($1, $2, $3, $4, $5, $6) `, uuid.New(), versionID, rejectorID, "rejected", &comment, now) if err != nil { return fmt.Errorf("failed to create approval record: %w", err) } return tx.Commit(ctx) } // GetApprovals returns approval history for a version func (s *EmailTemplateService) GetApprovals(ctx context.Context, versionID uuid.UUID) ([]models.EmailTemplateApproval, error) { rows, err := s.db.Query(ctx, ` SELECT id, version_id, approver_id, action, comment, created_at FROM email_template_approvals WHERE version_id = $1 ORDER BY created_at DESC `, versionID) if err != nil { return nil, fmt.Errorf("failed to get approvals: %w", err) } defer rows.Close() var approvals []models.EmailTemplateApproval for rows.Next() { var a models.EmailTemplateApproval err := rows.Scan(&a.ID, &a.VersionID, &a.ApproverID, &a.Action, &a.Comment, &a.CreatedAt) if err != nil { return nil, fmt.Errorf("failed to scan approval: %w", err) } approvals = append(approvals, a) } return approvals, nil } // GetSettings returns global email settings func (s *EmailTemplateService) GetSettings(ctx context.Context) (*models.EmailTemplateSettings, error) { var settings models.EmailTemplateSettings err := s.db.QueryRow(ctx, ` SELECT id, logo_url, logo_base64, company_name, sender_name, sender_email, reply_to_email, footer_html, footer_text, primary_color, secondary_color, updated_at, updated_by FROM email_template_settings LIMIT 1 `).Scan(&settings.ID, &settings.LogoURL, &settings.LogoBase64, &settings.CompanyName, &settings.SenderName, &settings.SenderEmail, &settings.ReplyToEmail, &settings.FooterHTML, &settings.FooterText, &settings.PrimaryColor, &settings.SecondaryColor, &settings.UpdatedAt, &settings.UpdatedBy) if err != nil { return nil, fmt.Errorf("failed to get email settings: %w", err) } return &settings, nil } // UpdateSettings updates global email settings func (s *EmailTemplateService) UpdateSettings(ctx context.Context, req *models.UpdateEmailTemplateSettingsRequest, updatedBy uuid.UUID) error { query := "UPDATE email_template_settings SET updated_at = $1, updated_by = $2" args := []interface{}{time.Now(), updatedBy} argIdx := 3 if req.LogoURL != nil { query += fmt.Sprintf(", logo_url = $%d", argIdx) args = append(args, *req.LogoURL) argIdx++ } if req.LogoBase64 != nil { query += fmt.Sprintf(", logo_base64 = $%d", argIdx) args = append(args, *req.LogoBase64) argIdx++ } if req.CompanyName != nil { query += fmt.Sprintf(", company_name = $%d", argIdx) args = append(args, *req.CompanyName) argIdx++ } if req.SenderName != nil { query += fmt.Sprintf(", sender_name = $%d", argIdx) args = append(args, *req.SenderName) argIdx++ } if req.SenderEmail != nil { query += fmt.Sprintf(", sender_email = $%d", argIdx) args = append(args, *req.SenderEmail) argIdx++ } if req.ReplyToEmail != nil { query += fmt.Sprintf(", reply_to_email = $%d", argIdx) args = append(args, *req.ReplyToEmail) argIdx++ } if req.FooterHTML != nil { query += fmt.Sprintf(", footer_html = $%d", argIdx) args = append(args, *req.FooterHTML) argIdx++ } if req.FooterText != nil { query += fmt.Sprintf(", footer_text = $%d", argIdx) args = append(args, *req.FooterText) argIdx++ } if req.PrimaryColor != nil { query += fmt.Sprintf(", primary_color = $%d", argIdx) args = append(args, *req.PrimaryColor) argIdx++ } if req.SecondaryColor != nil { query += fmt.Sprintf(", secondary_color = $%d", argIdx) args = append(args, *req.SecondaryColor) argIdx++ } _, err := s.db.Exec(ctx, query, args...) if err != nil { return fmt.Errorf("failed to update settings: %w", err) } return nil } // RenderTemplate renders a template with variables func (s *EmailTemplateService) RenderTemplate(version *models.EmailTemplateVersion, variables map[string]string) (*models.EmailPreviewResponse, error) { subject := version.Subject bodyHTML := version.BodyHTML bodyText := version.BodyText // Replace variables in format {{variable_name}} re := regexp.MustCompile(`\{\{(\w+)\}\}`) replaceFunc := func(content string) string { return re.ReplaceAllStringFunc(content, func(match string) string { varName := strings.Trim(match, "{}") if val, ok := variables[varName]; ok { return val } return match // Keep placeholder if variable not provided }) } return &models.EmailPreviewResponse{ Subject: replaceFunc(subject), BodyHTML: replaceFunc(bodyHTML), BodyText: replaceFunc(bodyText), }, nil } // LogEmailSend logs a sent email func (s *EmailTemplateService) LogEmailSend(ctx context.Context, log *models.EmailSendLog) error { _, err := s.db.Exec(ctx, ` INSERT INTO email_send_logs (id, user_id, version_id, recipient, subject, status, error_msg, variables, sent_at, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) `, log.ID, log.UserID, log.VersionID, log.Recipient, log.Subject, log.Status, log.ErrorMsg, log.Variables, log.SentAt, log.CreatedAt) if err != nil { return fmt.Errorf("failed to log email send: %w", err) } return nil } // GetEmailStats returns email statistics func (s *EmailTemplateService) GetEmailStats(ctx context.Context) (*models.EmailStats, error) { var stats models.EmailStats err := s.db.QueryRow(ctx, ` SELECT COUNT(*) as total_sent, COUNT(*) FILTER (WHERE status = 'delivered') as delivered, COUNT(*) FILTER (WHERE status = 'bounced') as bounced, COUNT(*) FILTER (WHERE status = 'failed') as failed, COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '7 days') as recent_sent FROM email_send_logs `).Scan(&stats.TotalSent, &stats.Delivered, &stats.Bounced, &stats.Failed, &stats.RecentSent) if err != nil { return nil, fmt.Errorf("failed to get email stats: %w", err) } if stats.TotalSent > 0 { stats.DeliveryRate = float64(stats.Delivered) / float64(stats.TotalSent) * 100 } return &stats, nil } // GetDefaultTemplateContent returns default content for a template type func (s *EmailTemplateService) GetDefaultTemplateContent(templateType, language string) (subject, bodyHTML, bodyText string) { // Default templates in German if language == "de" { switch templateType { case models.EmailTypeWelcome: return s.getWelcomeTemplateDE() case models.EmailTypeEmailVerification: return s.getEmailVerificationTemplateDE() case models.EmailTypePasswordReset: return s.getPasswordResetTemplateDE() case models.EmailTypePasswordChanged: return s.getPasswordChangedTemplateDE() case models.EmailType2FAEnabled: return s.get2FAEnabledTemplateDE() case models.EmailType2FADisabled: return s.get2FADisabledTemplateDE() case models.EmailTypeNewDeviceLogin: return s.getNewDeviceLoginTemplateDE() case models.EmailTypeSuspiciousActivity: return s.getSuspiciousActivityTemplateDE() case models.EmailTypeAccountLocked: return s.getAccountLockedTemplateDE() case models.EmailTypeAccountUnlocked: return s.getAccountUnlockedTemplateDE() case models.EmailTypeDeletionRequested: return s.getDeletionRequestedTemplateDE() case models.EmailTypeDeletionConfirmed: return s.getDeletionConfirmedTemplateDE() case models.EmailTypeDataExportReady: return s.getDataExportReadyTemplateDE() case models.EmailTypeEmailChanged: return s.getEmailChangedTemplateDE() case models.EmailTypeNewVersionPublished: return s.getNewVersionPublishedTemplateDE() case models.EmailTypeConsentReminder: return s.getConsentReminderTemplateDE() case models.EmailTypeConsentDeadlineWarning: return s.getConsentDeadlineWarningTemplateDE() case models.EmailTypeAccountSuspended: return s.getAccountSuspendedTemplateDE() } } // Default English fallback return "No template", "

No template available

", "No template available" } // ======================================== // Default German Templates // ======================================== func (s *EmailTemplateService) getWelcomeTemplateDE() (string, string, string) { subject := "Willkommen bei BreakPilot!" bodyHTML := `

Willkommen bei BreakPilot!

Hallo {{user_name}},

vielen Dank für Ihre Registrierung bei BreakPilot. Ihr Konto wurde erfolgreich erstellt.

Sie können sich jetzt mit Ihrer E-Mail-Adresse {{user_email}} anmelden:

Jetzt anmelden

Bei Fragen stehen wir Ihnen gerne zur Verfügung unter {{support_email}}.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Willkommen bei BreakPilot! Hallo {{user_name}}, vielen Dank für Ihre Registrierung bei BreakPilot. Ihr Konto wurde erfolgreich erstellt. Sie können sich jetzt mit Ihrer E-Mail-Adresse {{user_email}} anmelden: {{login_url}} Bei Fragen stehen wir Ihnen gerne zur Verfügung unter {{support_email}}. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getEmailVerificationTemplateDE() (string, string, string) { subject := "Bitte bestätigen Sie Ihre E-Mail-Adresse" bodyHTML := `

E-Mail-Adresse bestätigen

Hallo {{user_name}},

bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Link klicken:

E-Mail bestätigen

Alternativ können Sie auch diesen Bestätigungscode eingeben: {{verification_code}}

Hinweis: Dieser Link ist nur {{expires_in}} gültig.

Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `E-Mail-Adresse bestätigen Hallo {{user_name}}, bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Link klicken: {{verification_url}} Alternativ können Sie auch diesen Bestätigungscode eingeben: {{verification_code}} Hinweis: Dieser Link ist nur {{expires_in}} gültig. Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getPasswordResetTemplateDE() (string, string, string) { subject := "Passwort zurücksetzen" bodyHTML := `

Passwort zurücksetzen

Hallo {{user_name}},

Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:

Neues Passwort festlegen

Alternativ können Sie auch diesen Code verwenden: {{reset_code}}

Hinweis: Dieser Link ist nur {{expires_in}} gültig.

Sicherheitshinweis: Diese Anfrage wurde von der IP-Adresse {{ip_address}} gestellt. Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail und Ihr Passwort bleibt unverändert.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Passwort zurücksetzen Hallo {{user_name}}, Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen: {{reset_url}} Alternativ können Sie auch diesen Code verwenden: {{reset_code}} Hinweis: Dieser Link ist nur {{expires_in}} gültig. Sicherheitshinweis: Diese Anfrage wurde von der IP-Adresse {{ip_address}} gestellt. Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail und Ihr Passwort bleibt unverändert. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getPasswordChangedTemplateDE() (string, string, string) { subject := "Ihr Passwort wurde geändert" bodyHTML := `

Passwort geändert

Hallo {{user_name}},

Ihr Passwort wurde am {{changed_at}} erfolgreich geändert.

Details:

Nicht Sie? Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Passwort geändert Hallo {{user_name}}, Ihr Passwort wurde am {{changed_at}} erfolgreich geändert. Details: - IP-Adresse: {{ip_address}} - Gerät: {{device_info}} Nicht Sie? Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) get2FAEnabledTemplateDE() (string, string, string) { subject := "Zwei-Faktor-Authentifizierung aktiviert" bodyHTML := `

2FA aktiviert

Hallo {{user_name}},

Die Zwei-Faktor-Authentifizierung wurde am {{enabled_at}} für Ihr Konto aktiviert.

Gerät: {{device_info}}

Sicherheitstipp: Bewahren Sie Ihre Recovery-Codes sicher auf. Sie benötigen diese, falls Sie den Zugang zu Ihrer Authenticator-App verlieren.

Sie können Ihre 2FA-Einstellungen jederzeit unter {{security_url}} verwalten.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `2FA aktiviert Hallo {{user_name}}, Die Zwei-Faktor-Authentifizierung wurde am {{enabled_at}} für Ihr Konto aktiviert. Gerät: {{device_info}} Sicherheitstipp: Bewahren Sie Ihre Recovery-Codes sicher auf. Sie benötigen diese, falls Sie den Zugang zu Ihrer Authenticator-App verlieren. Sie können Ihre 2FA-Einstellungen jederzeit unter {{security_url}} verwalten. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) get2FADisabledTemplateDE() (string, string, string) { subject := "Zwei-Faktor-Authentifizierung deaktiviert" bodyHTML := `

2FA deaktiviert

Hallo {{user_name}},

Die Zwei-Faktor-Authentifizierung wurde am {{disabled_at}} für Ihr Konto deaktiviert.

IP-Adresse: {{ip_address}}

Warnung: Ihr Konto ist jetzt weniger sicher. Wir empfehlen dringend, 2FA wieder zu aktivieren.

Sie können 2FA jederzeit unter {{security_url}} wieder aktivieren.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `2FA deaktiviert Hallo {{user_name}}, Die Zwei-Faktor-Authentifizierung wurde am {{disabled_at}} für Ihr Konto deaktiviert. IP-Adresse: {{ip_address}} Warnung: Ihr Konto ist jetzt weniger sicher. Wir empfehlen dringend, 2FA wieder zu aktivieren. Sie können 2FA jederzeit unter {{security_url}} wieder aktivieren. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getNewDeviceLoginTemplateDE() (string, string, string) { subject := "Neuer Login auf Ihrem Konto" bodyHTML := `

Neuer Login erkannt

Hallo {{user_name}},

Wir haben einen Login auf Ihrem Konto von einem neuen Gerät oder Standort erkannt:

Nicht Sie? Falls Sie diesen Login nicht durchgeführt haben, ändern Sie sofort Ihr Passwort unter {{security_url}}.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Neuer Login erkannt Hallo {{user_name}}, Wir haben einen Login auf Ihrem Konto von einem neuen Gerät oder Standort erkannt: - Zeitpunkt: {{login_time}} - IP-Adresse: {{ip_address}} - Gerät: {{device_info}} - Standort: {{location}} Nicht Sie? Falls Sie diesen Login nicht durchgeführt haben, ändern Sie sofort Ihr Passwort unter {{security_url}}. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getSuspiciousActivityTemplateDE() (string, string, string) { subject := "Verdächtige Aktivität auf Ihrem Konto" bodyHTML := `

Verdächtige Aktivität erkannt

Hallo {{user_name}},

Wir haben verdächtige Aktivität auf Ihrem Konto festgestellt:

Wichtig: Bitte überprüfen Sie Ihre Sicherheitseinstellungen unter {{security_url}} und ändern Sie Ihr Passwort, falls Sie diese Aktivität nicht selbst durchgeführt haben.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Verdächtige Aktivität erkannt Hallo {{user_name}}, Wir haben verdächtige Aktivität auf Ihrem Konto festgestellt: - Art: {{activity_type}} - Zeitpunkt: {{activity_time}} - IP-Adresse: {{ip_address}} Wichtig: Bitte überprüfen Sie Ihre Sicherheitseinstellungen unter {{security_url}} und ändern Sie Ihr Passwort, falls Sie diese Aktivität nicht selbst durchgeführt haben. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getAccountLockedTemplateDE() (string, string, string) { subject := "Ihr Konto wurde gesperrt" bodyHTML := `

Konto gesperrt

Hallo {{user_name}},

Ihr Konto wurde am {{locked_at}} aus folgendem Grund gesperrt:

{{reason}}

Ihr Konto wird automatisch entsperrt am: {{unlock_time}}

Falls Sie Hilfe benötigen, kontaktieren Sie uns unter {{support_url}}.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Konto gesperrt Hallo {{user_name}}, Ihr Konto wurde am {{locked_at}} aus folgendem Grund gesperrt: {{reason}} Ihr Konto wird automatisch entsperrt am: {{unlock_time}} Falls Sie Hilfe benötigen, kontaktieren Sie uns unter {{support_url}}. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getAccountUnlockedTemplateDE() (string, string, string) { subject := "Ihr Konto wurde entsperrt" bodyHTML := `

Konto entsperrt

Hallo {{user_name}},

Ihr Konto wurde am {{unlocked_at}} erfolgreich entsperrt.

Jetzt anmelden

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Konto entsperrt Hallo {{user_name}}, Ihr Konto wurde am {{unlocked_at}} erfolgreich entsperrt. Sie können sich jetzt wieder anmelden: {{login_url}} Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getDeletionRequestedTemplateDE() (string, string, string) { subject := "Bestätigung: Kontolöschung angefordert" bodyHTML := `

Kontolöschung angefordert

Hallo {{user_name}},

Sie haben am {{requested_at}} die Löschung Ihres Kontos beantragt.

Wichtig: Ihr Konto und alle zugehörigen Daten werden endgültig am {{deletion_date}} gelöscht.

Folgende Daten werden gelöscht:

{{data_info}}

Sie können die Löschung bis zum genannten Datum abbrechen:

Löschung abbrechen

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Kontolöschung angefordert Hallo {{user_name}}, Sie haben am {{requested_at}} die Löschung Ihres Kontos beantragt. Wichtig: Ihr Konto und alle zugehörigen Daten werden endgültig am {{deletion_date}} gelöscht. Folgende Daten werden gelöscht: {{data_info}} Sie können die Löschung bis zum genannten Datum abbrechen: {{cancel_url}} Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getDeletionConfirmedTemplateDE() (string, string, string) { subject := "Ihr Konto wurde gelöscht" bodyHTML := `

Konto gelöscht

Hallo {{user_name}},

Ihr Konto und alle zugehörigen Daten wurden am {{deleted_at}} erfolgreich und endgültig gelöscht.

Wir bedauern, dass Sie uns verlassen. Falls Sie uns Feedback geben möchten:

Feedback geben

Vielen Dank für Ihre Zeit bei BreakPilot.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Konto gelöscht Hallo {{user_name}}, Ihr Konto und alle zugehörigen Daten wurden am {{deleted_at}} erfolgreich und endgültig gelöscht. Wir bedauern, dass Sie uns verlassen. Falls Sie uns Feedback geben möchten: {{feedback_url}} Vielen Dank für Ihre Zeit bei BreakPilot. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getDataExportReadyTemplateDE() (string, string, string) { subject := "Ihr Datenexport ist bereit" bodyHTML := `

Datenexport bereit

Hallo {{user_name}},

Ihr angeforderte Datenexport wurde erstellt und steht zum Download bereit.

Daten herunterladen ({{file_size}})

Hinweis: Der Download-Link ist nur {{expires_in}} gültig.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Datenexport bereit Hallo {{user_name}}, Ihr angeforderte Datenexport wurde erstellt und steht zum Download bereit: {{download_url}} Dateigröße: {{file_size}} Hinweis: Der Download-Link ist nur {{expires_in}} gültig. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getEmailChangedTemplateDE() (string, string, string) { subject := "Ihre E-Mail-Adresse wurde geändert" bodyHTML := `

E-Mail-Adresse geändert

Hallo {{user_name}},

Die E-Mail-Adresse Ihres Kontos wurde am {{changed_at}} geändert.

Nicht Sie? Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}.

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `E-Mail-Adresse geändert Hallo {{user_name}}, Die E-Mail-Adresse Ihres Kontos wurde am {{changed_at}} geändert. - Alte Adresse: {{old_email}} - Neue Adresse: {{new_email}} Nicht Sie? Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}. Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getNewVersionPublishedTemplateDE() (string, string, string) { subject := "Neue Version: {{document_name}} - Ihre Zustimmung ist erforderlich" bodyHTML := `

Neue Dokumentversion

Hallo {{user_name}},

Eine neue Version unserer {{document_name}} ({{document_type}}) wurde veröffentlicht.

Version: {{version}}

Bitte prüfen und bestätigen Sie die aktualisierte Version bis zum {{deadline}}.

Jetzt prüfen und zustimmen

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Neue Dokumentversion Hallo {{user_name}}, Eine neue Version unserer {{document_name}} ({{document_type}}) wurde veröffentlicht. Version: {{version}} Bitte prüfen und bestätigen Sie die aktualisierte Version bis zum {{deadline}}. Jetzt prüfen und zustimmen: {{consent_url}} Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getConsentReminderTemplateDE() (string, string, string) { subject := "Erinnerung: Zustimmung zu {{document_name}} erforderlich" bodyHTML := `

Erinnerung: Zustimmung erforderlich

Hallo {{user_name}},

Dies ist eine freundliche Erinnerung, dass Ihre Zustimmung zur {{document_name}} noch aussteht.

Noch {{days_left}} Tage bis zur Frist am {{deadline}}.

Jetzt zustimmen

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Erinnerung: Zustimmung erforderlich Hallo {{user_name}}, Dies ist eine freundliche Erinnerung, dass Ihre Zustimmung zur {{document_name}} noch aussteht. Noch {{days_left}} Tage bis zur Frist am {{deadline}}. Jetzt zustimmen: {{consent_url}} Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getConsentDeadlineWarningTemplateDE() (string, string, string) { subject := "DRINGEND: Zustimmung zu {{document_name}} läuft bald ab" bodyHTML := `

Dringende Erinnerung

Hallo {{user_name}},

Ihre Frist zur Zustimmung zur {{document_name}} läuft in {{hours_left}} ab!

Wichtig: {{consequences}}

Sofort zustimmen

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Dringende Erinnerung Hallo {{user_name}}, Ihre Frist zur Zustimmung zur {{document_name}} läuft in {{hours_left}} ab! Wichtig: {{consequences}} Sofort zustimmen: {{consent_url}} Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } func (s *EmailTemplateService) getAccountSuspendedTemplateDE() (string, string, string) { subject := "Ihr Konto wurde suspendiert" bodyHTML := `

Konto suspendiert

Hallo {{user_name}},

Ihr Konto wurde am {{suspended_at}} suspendiert.

Grund: {{reason}}

Fehlende Zustimmungen:

{{documents}}

Um Ihr Konto zu reaktivieren, stimmen Sie bitte den ausstehenden Dokumenten zu:

Konto reaktivieren

Mit freundlichen Grüßen,
Ihr BreakPilot-Team

` bodyText := `Konto suspendiert Hallo {{user_name}}, Ihr Konto wurde am {{suspended_at}} suspendiert. Grund: {{reason}} Fehlende Zustimmungen: {{documents}} Um Ihr Konto zu reaktivieren, stimmen Sie bitte den ausstehenden Dokumenten zu: {{consent_url}} Mit freundlichen Grüßen, Ihr BreakPilot-Team` return subject, bodyHTML, bodyText } // InitDefaultTemplates creates default email templates if they don't exist func (s *EmailTemplateService) InitDefaultTemplates(ctx context.Context) error { templateTypes := []struct { Type string Name string Description string SortOrder int }{ {models.EmailTypeWelcome, "Willkommens-E-Mail", "Wird nach erfolgreicher Registrierung gesendet", 1}, {models.EmailTypeEmailVerification, "E-Mail-Verifizierung", "Enthält Link zur E-Mail-Bestätigung", 2}, {models.EmailTypePasswordReset, "Passwort zurücksetzen", "Enthält Link zum Passwort-Reset", 3}, {models.EmailTypePasswordChanged, "Passwort geändert", "Bestätigung der Passwortänderung", 4}, {models.EmailType2FAEnabled, "2FA aktiviert", "Bestätigung der 2FA-Aktivierung", 5}, {models.EmailType2FADisabled, "2FA deaktiviert", "Warnung über 2FA-Deaktivierung", 6}, {models.EmailTypeNewDeviceLogin, "Neuer Login", "Benachrichtigung über Login von neuem Gerät", 7}, {models.EmailTypeSuspiciousActivity, "Verdächtige Aktivität", "Warnung über verdächtige Kontoaktivität", 8}, {models.EmailTypeAccountLocked, "Konto gesperrt", "Benachrichtigung über Kontosperrung", 9}, {models.EmailTypeAccountUnlocked, "Konto entsperrt", "Bestätigung der Kontoentsperrung", 10}, {models.EmailTypeDeletionRequested, "Löschung angefordert", "Bestätigung der Löschanfrage", 11}, {models.EmailTypeDeletionConfirmed, "Löschung bestätigt", "Bestätigung der Kontolöschung", 12}, {models.EmailTypeDataExportReady, "Datenexport bereit", "Benachrichtigung über fertigen Datenexport", 13}, {models.EmailTypeEmailChanged, "E-Mail geändert", "Bestätigung der E-Mail-Änderung", 14}, {models.EmailTypeNewVersionPublished, "Neue Version veröffentlicht", "Benachrichtigung über neue Dokumentversion", 15}, {models.EmailTypeConsentReminder, "Zustimmungs-Erinnerung", "Erinnerung an ausstehende Zustimmung", 16}, {models.EmailTypeConsentDeadlineWarning, "Frist-Warnung", "Dringende Warnung vor ablaufender Frist", 17}, {models.EmailTypeAccountSuspended, "Konto suspendiert", "Benachrichtigung über Kontosuspendierung", 18}, } for _, tt := range templateTypes { // Check if template exists var exists bool err := s.db.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM email_templates WHERE type = $1)`, tt.Type).Scan(&exists) if err != nil { return fmt.Errorf("failed to check template existence: %w", err) } if !exists { desc := tt.Description _, err = s.db.Exec(ctx, ` INSERT INTO email_templates (id, type, name, description, is_active, sort_order, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) `, uuid.New(), tt.Type, tt.Name, &desc, true, tt.SortOrder, time.Now(), time.Now()) if err != nil { return fmt.Errorf("failed to create template %s: %w", tt.Type, err) } // Create default German version template, err := s.GetTemplateByType(ctx, tt.Type) if err != nil { return fmt.Errorf("failed to get template %s: %w", tt.Type, err) } subject, bodyHTML, bodyText := s.GetDefaultTemplateContent(tt.Type, "de") _, err = s.db.Exec(ctx, ` INSERT INTO email_template_versions (id, template_id, version, language, subject, body_html, body_text, status, published_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) `, uuid.New(), template.ID, "1.0.0", "de", subject, bodyHTML, bodyText, "published", time.Now(), time.Now(), time.Now()) if err != nil { return fmt.Errorf("failed to create template version %s: %w", tt.Type, err) } } } return nil } // GetSendLogs returns email send logs with optional filtering func (s *EmailTemplateService) GetSendLogs(ctx context.Context, limit, offset int) ([]models.EmailSendLog, int, error) { var total int err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM email_send_logs`).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("failed to count send logs: %w", err) } rows, err := s.db.Query(ctx, ` SELECT id, user_id, version_id, recipient, subject, status, error_msg, variables, sent_at, delivered_at, created_at FROM email_send_logs ORDER BY created_at DESC LIMIT $1 OFFSET $2 `, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to get send logs: %w", err) } defer rows.Close() var logs []models.EmailSendLog for rows.Next() { var log models.EmailSendLog err := rows.Scan(&log.ID, &log.UserID, &log.VersionID, &log.Recipient, &log.Subject, &log.Status, &log.ErrorMsg, &log.Variables, &log.SentAt, &log.DeliveredAt, &log.CreatedAt) if err != nil { return nil, 0, fmt.Errorf("failed to scan send log: %w", err) } logs = append(logs, log) } return logs, total, nil } // SendEmail sends an email using the specified template (stub - actual sending would use SMTP) func (s *EmailTemplateService) SendEmail(ctx context.Context, templateType, language, recipient string, variables map[string]string, userID *uuid.UUID) error { // Get published version version, err := s.GetPublishedVersion(ctx, templateType, language) if err != nil { return fmt.Errorf("failed to get published version: %w", err) } // Render template rendered, err := s.RenderTemplate(version, variables) if err != nil { return fmt.Errorf("failed to render template: %w", err) } // Log the send attempt variablesJSON, _ := json.Marshal(variables) now := time.Now() log := &models.EmailSendLog{ ID: uuid.New(), UserID: userID, VersionID: version.ID, Recipient: recipient, Subject: rendered.Subject, Status: "queued", Variables: ptr(string(variablesJSON)), CreatedAt: now, } if err := s.LogEmailSend(ctx, log); err != nil { return fmt.Errorf("failed to log email send: %w", err) } // TODO: Actual email sending via SMTP would go here // For now, we just log it as "sent" _, err = s.db.Exec(ctx, ` UPDATE email_send_logs SET status = 'sent', sent_at = $1 WHERE id = $2 `, now, log.ID) if err != nil { return fmt.Errorf("failed to update send log status: %w", err) } return nil } func ptr(s string) *string { return &s }