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) } // SendGoogleWelcomeEmail sends a welcome email for Google Sign In users (no verification needed) func (s *EmailService) SendGoogleWelcomeEmail(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) } // SendPostVerificationEmail sends a welcome email after user verifies their email address func (s *EmailService) SendPostVerificationEmail(to, firstName string) error { subject := "You're All Set! Getting Started with Casera" name := firstName if name == "" { name = "there" } bodyContent := fmt.Sprintf(` %s

Hi %s,

Your email is now verified and your Casera account is ready to go! Here are some tips to help you get the most out of the app.

🏠 Start with Your Property

Add your home or rental property to begin tracking everything in one place. You can add multiple properties and even share access with family members or co-owners.

📅 Set Up Recurring Tasks

Create recurring tasks for regular maintenance like HVAC filter changes, gutter cleaning, or lawn care. Casera will remind you when they're due so nothing falls through the cracks.

📝 Track Your Maintenance History

When you complete a task, add notes and photos to build a maintenance history. This is invaluable for warranty claims, selling your home, or just remembering when something was last serviced.

📄 Store Important Documents

Upload warranties, appliance manuals, insurance policies, and receipts. When you need them, they'll be right at your fingertips instead of buried in a drawer somewhere.

👷 Save Your Contractors

Add your trusted plumber, electrician, and other service providers to your contractor list. You'll never have to dig through old emails or papers to find their contact info again.

We're excited to have you on board. If you have any questions or feedback, we'd love to hear from you at support@casera.app.

Happy homeowning!
The Casera Team

%s`, emailHeader("You're Verified!"), name, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` You're All Set! Getting Started with Casera Hi %s, Your email is now verified and your Casera account is ready to go! Here are some tips to help you get the most out of the app. 1. START WITH YOUR PROPERTY Add your home or rental property to begin tracking everything in one place. You can add multiple properties and even share access with family members or co-owners. 2. SET UP RECURRING TASKS Create recurring tasks for regular maintenance like HVAC filter changes, gutter cleaning, or lawn care. Casera will remind you when they're due so nothing falls through the cracks. 3. TRACK YOUR MAINTENANCE HISTORY When you complete a task, add notes and photos to build a maintenance history. This is invaluable for warranty claims, selling your home, or just remembering when something was last serviced. 4. STORE IMPORTANT DOCUMENTS Upload warranties, appliance manuals, insurance policies, and receipts. When you need them, they'll be right at your fingertips instead of buried in a drawer somewhere. 5. SAVE YOUR CONTRACTORS Add your trusted plumber, electrician, and other service providers to your contractor list. You'll never have to dig through old emails or papers to find their contact info again. We're excited to have you on board. If you have any questions or feedback, we'd love to hear from you at support@casera.app. Happy homeowning! 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) } // SendNoResidenceOnboardingEmail sends an onboarding email to users who haven't created a residence func (s *EmailService) SendNoResidenceOnboardingEmail(to, firstName, baseURL, trackingID string) error { subject := "Get started with Casera - Add your first property" name := firstName if name == "" { name = "there" } trackingPixel := fmt.Sprintf(``, baseURL, trackingID) bodyContent := fmt.Sprintf(` %s

Hi %s,

We noticed you haven't added your first property to Casera yet. Adding a property is the first step to staying on top of your home maintenance!

Why add a property?

🏠 Track all your homes - Manage single-family homes, apartments, or investment properties

📅 Never miss maintenance - Set up recurring tasks with smart reminders

📄 Store important documents - Keep warranties, manuals, and records in one place

👷 Manage contractors - Keep your trusted pros organized and accessible

Add Your First Property

Just open the Casera app and tap the + button to get started. It only takes a minute!

Best regards,
The Casera Team

%s %s`, emailHeader("Get Started!"), name, trackingPixel, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Get started with Casera - Add your first property Hi %s, We noticed you haven't added your first property to Casera yet. Adding a property is the first step to staying on top of your home maintenance! Why add a property? - Track all your homes: Manage single-family homes, apartments, or investment properties - Never miss maintenance: Set up recurring tasks with smart reminders - Store important documents: Keep warranties, manuals, and records in one place - Manage contractors: Keep your trusted pros organized and accessible Just open the Casera app and tap the + button to get started. It only takes a minute! Best regards, The Casera Team `, name) return s.SendEmail(to, subject, htmlBody, textBody) } // SendNoTasksOnboardingEmail sends an onboarding email to users who have a property but no tasks func (s *EmailService) SendNoTasksOnboardingEmail(to, firstName, baseURL, trackingID string) error { subject := "Stay on top of home maintenance with Casera" name := firstName if name == "" { name = "there" } trackingPixel := fmt.Sprintf(``, baseURL, trackingID) bodyContent := fmt.Sprintf(` %s

Hi %s,

Great job adding your property to Casera! Now it's time to set up your first maintenance task and start tracking your home care.

Task ideas to get you started:

🌡️ HVAC Filter Replacement - Monthly or quarterly

💧 Water Heater Flush - Annually

🌿 Lawn Care - Weekly or bi-weekly

🕶 Gutter Cleaning - Seasonal

🔥 Smoke Detector Test - Monthly

Create Your First Task

Set up recurring tasks and Casera will remind you when they're due. No more forgotten maintenance!

Best regards,
The Casera Team

%s %s`, emailHeader("Track Your Tasks!"), name, trackingPixel, emailFooter(time.Now().Year())) htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent) textBody := fmt.Sprintf(` Stay on top of home maintenance with Casera Hi %s, Great job adding your property to Casera! Now it's time to set up your first maintenance task and start tracking your home care. Task ideas to get you started: - HVAC Filter Replacement: Monthly or quarterly - Water Heater Flush: Annually - Lawn Care: Weekly or bi-weekly - Gutter Cleaning: Seasonal - Smoke Detector Test: Monthly Set up recurring tasks and Casera will remind you when they're due. No more forgotten maintenance! Best regards, The Casera Team `, name) return s.SendEmail(to, subject, htmlBody, textBody) } // 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 }