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 := `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:
Bei Fragen stehen wir Ihnen gerne zur Verfügung unter {{support_email}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
Hallo {{user_name}},
bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Link klicken:
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
Hallo {{user_name}},
Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:
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
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
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
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
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
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
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
Hallo {{user_name}},
Ihr Konto wurde am {{unlocked_at}} erfolgreich entsperrt.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
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:
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
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:
Vielen Dank für Ihre Zeit bei BreakPilot.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
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
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
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}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
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}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
Hallo {{user_name}},
Ihre Frist zur Zustimmung zur {{document_name}} läuft in {{hours_left}} ab!
Wichtig: {{consequences}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team
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:
Mit freundlichen Grüßen,
Ihr BreakPilot-Team