package services import ( "fmt" "html" "net/url" "strings" "goyco/internal/config" "goyco/internal/database" ) type EmailService struct { EmailSender EmailSender baseURL string config *config.Config } func NewEmailService(cfg *config.Config, sender EmailSender) (*EmailService, error) { baseURL := strings.TrimRight(strings.TrimSpace(cfg.App.BaseURL), "/") if baseURL == "" { return nil, fmt.Errorf("APP_BASE_URL is required and must be externally reachable") } return &EmailService{ EmailSender: sender, baseURL: baseURL, config: cfg, }, nil } func (s *EmailService) SendVerificationEmail(user *database.User, token string) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } verificationURL := fmt.Sprintf("%s/confirm?token=%s", s.baseURL, url.QueryEscape(token)) subject := fmt.Sprintf("🎉 Welcome to %s! Confirm your email address", s.config.App.Title) body := s.GenerateVerificationEmailBody(user.Username, verificationURL) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send verification email: %w", err) } return nil } func (s *EmailService) SendEmailChangeVerificationEmail(user *database.User, token string) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } verificationURL := fmt.Sprintf("%s/confirm?token=%s", s.baseURL, url.QueryEscape(token)) subject := "📧 Confirm your new email address" body := s.GenerateEmailChangeVerificationEmailBody(user.Username, verificationURL) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send email change verification email: %w", err) } return nil } func (s *EmailService) SendResendVerificationEmail(user *database.User, token string) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } verificationURL := fmt.Sprintf("%s/confirm?token=%s", s.baseURL, url.QueryEscape(token)) subject := fmt.Sprintf("🔄 Resend: Confirm your %s account", s.config.App.Title) body := s.GenerateResendVerificationEmailBody(user.Username, verificationURL) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send verification email: %w", err) } return nil } func (s *EmailService) SendPasswordResetEmail(user *database.User, token string) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.baseURL, url.QueryEscape(token)) subject := fmt.Sprintf("Reset your %s password", s.config.App.Title) body := s.GeneratePasswordResetEmailBody(user.Username, resetURL) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send password reset email: %w", err) } return nil } func (s *EmailService) SendAccountDeletionEmail(user *database.User, token string) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } confirmationURL := fmt.Sprintf("%s/settings/delete/confirm?token=%s", s.baseURL, url.QueryEscape(token)) subject := "Confirm Account Deletion" body := s.GenerateAccountDeletionEmailBody(user.Username, confirmationURL) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send account deletion email: %w", err) } return nil } func (s *EmailService) SendAccountDeletionNotificationEmail(user *database.User, deletedPosts bool) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } subject, body := GenerateAccountDeletionNotificationEmail(user.Username, s.config.App.AdminEmail, s.baseURL, s.config.App.Title, deletedPosts) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send account deletion notification email: %w", err) } return nil } func (s *EmailService) SendAccountLockNotificationEmail(user *database.User) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } subject, body := GenerateAccountLockNotificationEmail(user.Username, s.config.App.AdminEmail, s.config.App.Title) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send account lock notification email: %w", err) } return nil } func (s *EmailService) SendAccountUnlockNotificationEmail(user *database.User) error { if s.EmailSender == nil { return ErrEmailSenderUnavailable } subject, body := GenerateAccountUnlockNotificationEmail(user.Username, s.config.App.AdminEmail, s.baseURL, s.config.App.Title) if err := s.EmailSender.Send(user.Email, subject, body); err != nil { return fmt.Errorf("send account unlock notification email: %w", err) } return nil } func (s *EmailService) GenerateVerificationEmailBody(username, verificationURL string) string { safeUsername := html.EscapeString(username) safeURL := html.EscapeString(verificationURL) siteTitle := s.config.App.Title return s.generateStyledEmailBody(siteTitle, "🎉 Welcome! Confirm your email address", fmt.Sprintf("Hello %s,", safeUsername), fmt.Sprintf("Welcome to %s! Please confirm your email address by clicking the link below:", siteTitle), safeURL, "Confirm Email Address", "If the link doesn't work, you can copy and paste it into your browser.\n\nOnce confirmed, you'll be able to:\n- Create and share posts with the community\n- Vote on content you find interesting\n- Connect with other members\n- Customize your profile and preferences\n\nIf you didn't create this account, you can safely ignore this email.") } func (s *EmailService) GenerateEmailChangeVerificationEmailBody(username, verificationURL string) string { safeUsername := html.EscapeString(username) safeURL := html.EscapeString(verificationURL) siteTitle := s.config.App.Title return s.generateStyledEmailBody(siteTitle, "📧 Confirm your new email address", fmt.Sprintf("Hello %s,", safeUsername), "You've requested to change your email address. To complete this change, please confirm your new email address by clicking the link below:\n\nIf the link doesn't work, you can copy and paste it into your browser.\n\nOnce confirmed, your new email address will be active and you'll need to use it for future logins.\n\nIf you didn't request this email change, please contact our support team immediately.", safeURL, "Confirm New Email Address", "") } func (s *EmailService) GenerateResendVerificationEmailBody(username, verificationURL string) string { safeUsername := html.EscapeString(username) safeURL := html.EscapeString(verificationURL) siteTitle := s.config.App.Title return s.generateStyledEmailBody(siteTitle, fmt.Sprintf("🔄 Resend: Confirm your %s account", siteTitle), fmt.Sprintf("Hello %s,", safeUsername), "We've sent you a new verification link.\n\nPlease confirm your email address by clicking the link below:\n\nIf you're having trouble with the verification link:\n- Check your spam/junk folder\n- Make sure you're clicking the most recent email\n- Try copying the link and pasting it in your browser\n- Contact support if the problem persists\n\nIf you didn't request this email, you can safely ignore this message.", safeURL, "Confirm Email Address", "") } func (s *EmailService) GeneratePasswordResetEmailBody(username, resetURL string) string { safeUsername := html.EscapeString(username) safeURL := html.EscapeString(resetURL) siteTitle := s.config.App.Title return s.generateStyledEmailBody(siteTitle, fmt.Sprintf("Reset your %s password", siteTitle), fmt.Sprintf("Hello %s,", safeUsername), "We received a request to reset your password. To reset it, click the link below:", safeURL, "Reset Password", "This link will expire in 24 hours. If you didn't make this request, you can safely ignore this message.") } func (s *EmailService) GenerateAccountDeletionEmailBody(username, confirmationURL string) string { safeUsername := html.EscapeString(username) safeURL := html.EscapeString(confirmationURL) siteTitle := s.config.App.Title return s.generateStyledEmailBody(siteTitle, "Confirm Account Deletion", fmt.Sprintf("Hello %s,", safeUsername), fmt.Sprintf("We received a request to delete your %s account.\n\nTo confirm, click the link below:", siteTitle), safeURL, "Confirm Account Deletion", "This link will expire in 24 hours.\n\nIf you didn't make this request, you can safely ignore this message.") } func (s *EmailService) generateStyledEmailBody(siteTitle, emailTitle, greeting, message, actionURL, actionText, footer string) string { safeAdminEmail := html.EscapeString(s.config.App.AdminEmail) var buttonHTML string if actionURL != "" && actionText != "" { buttonHTML = fmt.Sprintf(`
`, actionURL, actionText) } return fmt.Sprintf(`