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
}
// 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"
}
htmlBody := fmt.Sprintf(`
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, time.Now().Year())
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"
}
htmlBody := fmt.Sprintf(`
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, time.Now().Year())
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"
}
htmlBody := 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, time.Now().Year())
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"
}
htmlBody := 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, time.Now().Year())
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"
}
htmlBody := 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, time.Now().UTC().Format("January 2, 2006 at 3:04 PM UTC"), time.Now().Year())
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, time.Now().UTC().Format("January 2, 2006 at 3:04 PM UTC"))
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"
}
htmlBody := fmt.Sprintf(`
Hi %s,
A task has been completed at %s:
%s
Completed by: %s
Completed on: %s
Best regards,
The Casera Team
`, name, residenceName, taskTitle, completedByName, time.Now().UTC().Format("January 2, 2006 at 3:04 PM"), time.Now().Year())
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, time.Now().UTC().Format("January 2, 2006 at 3:04 PM"))
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"
}
htmlBody := fmt.Sprintf(`
Hi %s,
Here's your tasks report for %s. The full report is attached as a PDF.
Summary
|
%d
Total Tasks
|
%d
Completed
|
%d
Pending
|
%d
Overdue
|
Open the attached PDF for the complete list of tasks with details.
Best regards,
The Casera Team
`, residenceName, name, residenceName, totalTasks, completed, pending, overdue, time.Now().Year())
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
}