Files
honeyDueAPI/internal/services/email_service.go
Trey t 3419b66097 Add landing page, redesign emails, and return updated task on completion
- Integrate landing page into Go app (served at root /)
- Add STATIC_DIR config for static file serving
- Redesign all email templates with modern dark theme styling
- Add app icon to email headers
- Return updated task with kanban_column in completion response
- Update task DTO to include kanban column for client-side state updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 21:33:17 -06:00

631 lines
32 KiB
Go

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 `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>%s</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(180deg, #0F172A 0%%, #1E293B 100%%); -webkit-font-smoothing: antialiased;">
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="background: linear-gradient(180deg, #0F172A 0%%, #1E293B 100%%);">
<tr>
<td style="padding: 40px 20px;">
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="max-width: 600px; margin: 0 auto; background: #1E293B; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);">
%s
</table>
</td>
</tr>
</table>
</body>
</html>`
}
// 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(`
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #0079FF 0%%, #5AC7F9 50%%, #14B8A6 100%%); padding: 40px 30px; text-align: center;">
<!-- Logo Icon -->
<table role="presentation" cellspacing="0" cellpadding="0" style="margin: 0 auto 16px auto;">
<tr>
<td style="background: rgba(255, 255, 255, 0.15); border-radius: 20px; padding: 4px;">
<img src="%s" alt="Casera" width="64" height="64" style="display: block; border-radius: 16px; border: 0;" />
</td>
</tr>
</table>
<h1 style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 28px; font-weight: 800; color: #FFFFFF; margin: 0; letter-spacing: -0.5px;">Casera</h1>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 20px; font-weight: 600; color: rgba(255, 255, 255, 0.95); margin: 12px 0 0 0;">%s</p>
</td>
</tr>`, emailIconURL, title)
}
// emailFooter returns the footer section
func emailFooter(year int) string {
return fmt.Sprintf(`
<!-- Footer -->
<tr>
<td style="background: #0F172A; padding: 30px; text-align: center;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: rgba(255, 255, 255, 0.5); margin: 0;">&copy; %d Casera. All rights reserved.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; color: rgba(255, 255, 255, 0.4); margin: 12px 0 0 0;">Never miss home maintenance again.</p>
</td>
</tr>`, 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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 20px 0;">Thank you for creating a Casera account! To complete your registration, please verify your email address by entering the code below:</p>
<!-- Code Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: linear-gradient(135deg, rgba(0, 121, 255, 0.1) 0%%, rgba(20, 184, 166, 0.1) 100%%); border: 2px solid rgba(0, 121, 255, 0.2); border-radius: 12px; padding: 24px; text-align: center;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 36px; font-weight: 800; letter-spacing: 8px; color: #0079FF; margin: 0;">%s</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: #9CA3AF; margin: 12px 0 0 0;">This code will expire in 24 hours</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 24px 0 0 0;">If you didn't create a Casera account, you can safely ignore this email.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">Thank you for joining Casera! Your account has been created and you're ready to start managing your properties.</p>
<!-- Features Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: #F8FAFC; border-radius: 12px; padding: 24px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0;">Here's what you can do with Casera:</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127968; <strong>Manage Properties</strong> - Track all your homes and rentals in one place</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#9989; <strong>Task Management</strong> - Never miss maintenance with smart scheduling</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128119; <strong>Contractor Directory</strong> - Keep your trusted pros organized</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128196; <strong>Document Storage</strong> - Store warranties, manuals, and important records</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 24px 0 0 0;">If you have any questions, feel free to reach out to us at support@casera.app.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 20px 0;">Please use the following code to verify your email address:</p>
<!-- Code Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: linear-gradient(135deg, rgba(0, 121, 255, 0.1) 0%%, rgba(20, 184, 166, 0.1) 100%%); border: 2px solid rgba(0, 121, 255, 0.2); border-radius: 12px; padding: 24px; text-align: center;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 36px; font-weight: 800; letter-spacing: 8px; color: #0079FF; margin: 0;">%s</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: #9CA3AF; margin: 12px 0 0 0;">This code will expire in 24 hours</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 24px 0 0 0;">If you didn't request this, you can safely ignore this email.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 20px 0;">We received a request to reset your password. Use the following code to complete the reset:</p>
<!-- Code Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: linear-gradient(135deg, rgba(0, 121, 255, 0.1) 0%%, rgba(20, 184, 166, 0.1) 100%%); border: 2px solid rgba(0, 121, 255, 0.2); border-radius: 12px; padding: 24px; text-align: center;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 36px; font-weight: 800; letter-spacing: 8px; color: #0079FF; margin: 0;">%s</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: #9CA3AF; margin: 12px 0 0 0;">This code will expire in 15 minutes</p>
</td>
</tr>
</table>
<!-- Warning Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 24px 0;">
<tr>
<td style="background: linear-gradient(135deg, rgba(255, 148, 0, 0.1) 0%%, rgba(236, 72, 153, 0.05) 100%%); border-left: 4px solid #FF9400; border-radius: 8px; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 700; color: #E68600; margin: 0 0 8px 0;">Security Notice</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">If you didn't request a password reset, please ignore this email. Your password will remain unchanged.</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">Your Casera password was successfully changed on <strong>%s</strong>.</p>
<!-- Warning Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 24px 0;">
<tr>
<td style="background: linear-gradient(135deg, rgba(255, 148, 0, 0.1) 0%%, rgba(236, 72, 153, 0.05) 100%%); border-left: 4px solid #FF9400; border-radius: 8px; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 700; color: #E68600; margin: 0 0 8px 0;">Didn't make this change?</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">If you didn't change your password, please contact us immediately at support@casera.app or reset your password.</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">A task has been completed at <strong>%s</strong>:</p>
<!-- Success Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%%, rgba(20, 184, 166, 0.1) 100%%); border-left: 4px solid #22C55E; border-radius: 8px; padding: 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 700; color: #15803d; margin: 0 0 8px 0;">%s</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: #6B7280; margin: 8px 0 0 0;">Completed by: %s<br>Completed on: %s</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">Here's your tasks report for <strong>%s</strong>. The full report is attached as a PDF.</p>
<!-- Summary Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 24px 0;">
<tr>
<td style="background: #F8FAFC; border-radius: 12px; padding: 24px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0;">Summary</p>
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td width="25%%" style="text-align: center; padding: 12px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 28px; font-weight: 800; color: #1a1a1a; margin: 0;">%d</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; font-weight: 600; color: #9CA3AF; text-transform: uppercase; letter-spacing: 0.5px; margin: 4px 0 0 0;">Total</p>
</td>
<td width="25%%" style="text-align: center; padding: 12px; border-left: 1px solid #E5E7EB;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 28px; font-weight: 800; color: #22C55E; margin: 0;">%d</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; font-weight: 600; color: #9CA3AF; text-transform: uppercase; letter-spacing: 0.5px; margin: 4px 0 0 0;">Completed</p>
</td>
<td width="25%%" style="text-align: center; padding: 12px; border-left: 1px solid #E5E7EB;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 28px; font-weight: 800; color: #FF9400; margin: 0;">%d</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; font-weight: 600; color: #9CA3AF; text-transform: uppercase; letter-spacing: 0.5px; margin: 4px 0 0 0;">Pending</p>
</td>
<td width="25%%" style="text-align: center; padding: 12px; border-left: 1px solid #E5E7EB;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 28px; font-weight: 800; color: #EF4444; margin: 0;">%d</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; font-weight: 600; color: #9CA3AF; text-transform: uppercase; letter-spacing: 0.5px; margin: 4px 0 0 0;">Overdue</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 0 0 24px 0;">Open the attached PDF for the complete list of tasks with details.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
}