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
`
}
// 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
%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)
}
// 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
|
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
|
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
}