package services import ( "bytes" "fmt" "html/template" "io" "time" "github.com/rs/zerolog/log" "gopkg.in/gomail.v2" "github.com/treytartt/casera-api/internal/config" ) // EmailService handles sending emails type EmailService struct { cfg *config.EmailConfig dialer *gomail.Dialer } // NewEmailService creates a new email service func NewEmailService(cfg *config.EmailConfig) *EmailService { dialer := gomail.NewDialer(cfg.Host, cfg.Port, cfg.User, cfg.Password) return &EmailService{ cfg: cfg, dialer: dialer, } } // SendEmail sends an email func (s *EmailService) SendEmail(to, subject, htmlBody, textBody string) error { m := gomail.NewMessage() m.SetHeader("From", s.cfg.From) m.SetHeader("To", to) m.SetHeader("Subject", subject) m.SetBody("text/plain", textBody) m.AddAlternative("text/html", htmlBody) if err := s.dialer.DialAndSend(m); err != nil { log.Error().Err(err).Str("to", to).Str("subject", subject).Msg("Failed to send email") return fmt.Errorf("failed to send email: %w", err) } log.Info().Str("to", to).Str("subject", subject).Msg("Email sent successfully") return nil } // EmailAttachment represents an email attachment type EmailAttachment struct { Filename string ContentType string Data []byte } // SendEmailWithAttachment sends an email with an attachment func (s *EmailService) SendEmailWithAttachment(to, subject, htmlBody, textBody string, attachment *EmailAttachment) error { m := gomail.NewMessage() m.SetHeader("From", s.cfg.From) m.SetHeader("To", to) m.SetHeader("Subject", subject) m.SetBody("text/plain", textBody) m.AddAlternative("text/html", htmlBody) if attachment != nil { m.Attach(attachment.Filename, gomail.SetCopyFunc(func(w io.Writer) error { _, err := w.Write(attachment.Data) return err }), gomail.SetHeader(map[string][]string{ "Content-Type": {attachment.ContentType}, }), ) } if err := s.dialer.DialAndSend(m); err != nil { log.Error().Err(err).Str("to", to).Str("subject", subject).Msg("Failed to send email with attachment") return fmt.Errorf("failed to send email: %w", err) } log.Info().Str("to", to).Str("subject", subject).Str("attachment", attachment.Filename).Msg("Email with attachment sent successfully") return nil } // baseEmailTemplate returns the styled email wrapper func baseEmailTemplate() string { return ` %s
%s
` } // emailIconURL is the URL for the email icon const emailIconURL = "https://casera.app/images/icon.png" // emailHeader returns the gradient header section with logo func emailHeader(title string) string { return fmt.Sprintf(`
Casera

Casera

%s

`, emailIconURL, title) } // emailFooter returns the footer section func emailFooter(year int) string { return fmt.Sprintf(`

© %d Casera. All rights reserved.

Never miss home maintenance again.

`, year) } // SendWelcomeEmail sends a welcome email with verification code func (s *EmailService) SendWelcomeEmail(to, firstName, code string) error { subject := "Welcome to Casera - Verify Your Email" name := firstName if name == "" { name = "there" } bodyContent := fmt.Sprintf(` %s

Hi %s,

Thank you for creating a Casera account! To complete your registration, please verify your email address by entering the code below:

%s

This code will expire in 24 hours

If you didn't create a Casera account, you can safely ignore this email.

Best regards,
The Casera Team

%s`, emailHeader("Welcome!"), name, code, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Welcome to Casera! Hi %s, Thank you for creating a Casera account. To complete your registration, please verify your email address by entering the following code: %s This code will expire in 24 hours. If you didn't create a Casera account, you can safely ignore this email. Best regards, The Casera Team `, name, code) return s.SendEmail(to, subject, htmlBody, textBody) } // SendAppleWelcomeEmail sends a welcome email for Apple Sign In users (no verification needed) func (s *EmailService) SendAppleWelcomeEmail(to, firstName string) error { subject := "Welcome to Casera!" name := firstName if name == "" { name = "there" } bodyContent := fmt.Sprintf(` %s

Hi %s,

Thank you for joining Casera! Your account has been created and you're ready to start managing your properties.

Here's what you can do with Casera:

🏠 Manage Properties - Track all your homes and rentals in one place

Task Management - Never miss maintenance with smart scheduling

👷 Contractor Directory - Keep your trusted pros organized

📄 Document Storage - Store warranties, manuals, and important records

If you have any questions, feel free to reach out to us at support@casera.app.

Best regards,
The Casera Team

%s`, emailHeader("Welcome!"), name, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Welcome to Casera! Hi %s, Thank you for joining Casera! Your account has been created and you're ready to start managing your properties. Here's what you can do with Casera: - Manage Properties: Track all your homes and rentals in one place - Task Management: Never miss maintenance with smart scheduling - Contractor Directory: Keep your trusted pros organized - Document Storage: Store warranties, manuals, and important records If you have any questions, feel free to reach out to us at support@casera.app. Best regards, The Casera Team `, name) return s.SendEmail(to, subject, htmlBody, textBody) } // SendVerificationEmail sends an email verification code func (s *EmailService) SendVerificationEmail(to, firstName, code string) error { subject := "Casera - Verify Your Email" name := firstName if name == "" { name = "there" } bodyContent := fmt.Sprintf(` %s

Hi %s,

Please use the following code to verify your email address:

%s

This code will expire in 24 hours

If you didn't request this, you can safely ignore this email.

Best regards,
The Casera Team

%s`, emailHeader("Verify Your Email"), name, code, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Verify Your Email Hi %s, Please use the following code to verify your email address: %s This code will expire in 24 hours. If you didn't request this, you can safely ignore this email. Best regards, The Casera Team `, name, code) return s.SendEmail(to, subject, htmlBody, textBody) } // SendPasswordResetEmail sends a password reset email func (s *EmailService) SendPasswordResetEmail(to, firstName, code string) error { subject := "Casera - Password Reset Request" name := firstName if name == "" { name = "there" } bodyContent := fmt.Sprintf(` %s

Hi %s,

We received a request to reset your password. Use the following code to complete the reset:

%s

This code will expire in 15 minutes

Security Notice

If you didn't request a password reset, please ignore this email. Your password will remain unchanged.

Best regards,
The Casera Team

%s`, emailHeader("Password Reset"), name, code, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Password Reset Request Hi %s, We received a request to reset your password. Use the following code to complete the reset: %s This code will expire in 15 minutes. SECURITY NOTICE: If you didn't request a password reset, please ignore this email. Your password will remain unchanged. Best regards, The Casera Team `, name, code) return s.SendEmail(to, subject, htmlBody, textBody) } // SendPasswordChangedEmail sends a password changed confirmation email func (s *EmailService) SendPasswordChangedEmail(to, firstName string) error { subject := "Casera - Your Password Has Been Changed" name := firstName if name == "" { name = "there" } changeTime := time.Now().UTC().Format("January 2, 2006 at 3:04 PM UTC") bodyContent := fmt.Sprintf(` %s

Hi %s,

Your Casera password was successfully changed on %s.

Didn't make this change?

If you didn't change your password, please contact us immediately at support@casera.app or reset your password.

Best regards,
The Casera Team

%s`, emailHeader("Password Changed"), name, changeTime, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Password Changed Hi %s, Your Casera password was successfully changed on %s. DIDN'T MAKE THIS CHANGE? If you didn't change your password, please contact us immediately at support@casera.app or reset your password. Best regards, The Casera Team `, name, changeTime) return s.SendEmail(to, subject, htmlBody, textBody) } // SendTaskCompletedEmail sends an email notification when a task is completed func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, completedByName, residenceName string) error { subject := fmt.Sprintf("Casera - Task Completed: %s", taskTitle) name := recipientName if name == "" { name = "there" } completedTime := time.Now().UTC().Format("January 2, 2006 at 3:04 PM") bodyContent := fmt.Sprintf(` %s

Hi %s,

A task has been completed at %s:

%s

Completed by: %s
Completed on: %s

Best regards,
The Casera Team

%s`, emailHeader("Task Completed!"), name, residenceName, taskTitle, completedByName, completedTime, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Task Completed! Hi %s, A task has been completed at %s: Task: %s Completed by: %s Completed on: %s Best regards, The Casera Team `, name, residenceName, taskTitle, completedByName, completedTime) return s.SendEmail(to, subject, htmlBody, textBody) } // SendTasksReportEmail sends a tasks report email with PDF attachment func (s *EmailService) SendTasksReportEmail(to, recipientName, residenceName string, totalTasks, completed, pending, overdue int, pdfData []byte) error { subject := fmt.Sprintf("Casera - Tasks Report for %s", residenceName) name := recipientName if name == "" { name = "there" } bodyContent := fmt.Sprintf(` %s

Hi %s,

Here's your tasks report for %s. The full report is attached as a PDF.

Summary

%d

Total

%d

Completed

%d

Pending

%d

Overdue

Open the attached PDF for the complete list of tasks with details.

Best regards,
The Casera Team

%s`, emailHeader("Tasks Report"), name, residenceName, totalTasks, completed, pending, overdue, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Tasks Report for %s Hi %s, Here's your tasks report for %s. The full report is attached as a PDF. Summary: - Total Tasks: %d - Completed: %d - Pending: %d - Overdue: %d Open the attached PDF for the complete list of tasks with details. Best regards, The Casera Team `, residenceName, name, residenceName, totalTasks, completed, pending, overdue) // Create filename with timestamp filename := fmt.Sprintf("tasks_report_%s_%s.pdf", residenceName, time.Now().Format("2006-01-02"), ) attachment := &EmailAttachment{ Filename: filename, ContentType: "application/pdf", Data: pdfData, } return s.SendEmailWithAttachment(to, subject, htmlBody, textBody, attachment) } // EmailTemplate represents an email template type EmailTemplate struct { name string template *template.Template } // ParseTemplate parses an email template from a string func ParseTemplate(name, tmpl string) (*EmailTemplate, error) { t, err := template.New(name).Parse(tmpl) if err != nil { return nil, err } return &EmailTemplate{name: name, template: t}, nil } // Execute executes the template with the given data func (t *EmailTemplate) Execute(data interface{}) (string, error) { var buf bytes.Buffer if err := t.template.Execute(&buf, data); err != nil { return "", err } return buf.String(), nil }