Files
honeyDueAPI/internal/services/email_service.go
Trey t 9761156597 Add onboarding email campaign system with post-verification welcome email
Implements automated onboarding emails to encourage user engagement:
- Post-verification welcome email with 5 tips (sent after email verification)
- "No Residence" email (2+ days after registration with no property)
- "No Tasks" email (5+ days after first residence with no tasks)

Key features:
- Each onboarding email type sent only once per user (enforced by unique constraint)
- Email open tracking via tracking pixel endpoint
- Daily scheduled job at 10:00 AM UTC to process eligible users
- Admin panel UI for viewing sent emails, stats, and manual sending
- Admin can send any email type to users from the user detail Testing section

New files:
- internal/models/onboarding_email.go - Database model with tracking
- internal/services/onboarding_email_service.go - Business logic and eligibility queries
- internal/handlers/tracking_handler.go - Email open tracking endpoint
- internal/admin/handlers/onboarding_handler.go - Admin API endpoints
- admin/src/app/(dashboard)/onboarding-emails/ - Admin UI pages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 14:36:50 -06:00

897 lines
51 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)
}
// 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
<!-- 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 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.</p>
<!-- Tip 1 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #F0FDF4; border-left: 4px solid #22C55E; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #166534; margin: 0 0 8px 0;">&#127968; Start with Your Property</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">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.</p>
</td>
</tr>
</table>
<!-- Tip 2 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #EFF6FF; border-left: 4px solid #3B82F6; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #1E40AF; margin: 0 0 8px 0;">&#128197; Set Up Recurring Tasks</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">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.</p>
</td>
</tr>
</table>
<!-- Tip 3 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #FEF3C7; border-left: 4px solid #F59E0B; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #92400E; margin: 0 0 8px 0;">&#128221; Track Your Maintenance History</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">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.</p>
</td>
</tr>
</table>
<!-- Tip 4 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #F5F3FF; border-left: 4px solid #8B5CF6; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #5B21B6; margin: 0 0 8px 0;">&#128196; Store Important Documents</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">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.</p>
</td>
</tr>
</table>
<!-- Tip 5 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 24px;">
<tr>
<td style="background: #FDF2F8; border-left: 4px solid #EC4899; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #9D174D; margin: 0 0 8px 0;">&#128119; Save Your Contractors</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">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.</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: 0;">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.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Happy homeowning!<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%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
<!-- 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)
}
// 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(`<img src="%s/api/track/open/%s" width="1" height="1" style="display:none;" alt="" />`, baseURL, trackingID)
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;">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!</p>
<!-- Benefits 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;">Why add a property?</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127968; <strong>Track all your homes</strong> - Manage single-family homes, apartments, or investment properties</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128197; <strong>Never miss maintenance</strong> - Set up recurring tasks with smart reminders</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128196; <strong>Store important documents</strong> - Keep warranties, manuals, and records 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;">&#128119; <strong>Manage contractors</strong> - Keep your trusted pros organized and accessible</p>
</td>
</tr>
</table>
<!-- CTA Button -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 32px 0;">
<tr>
<td style="text-align: center;">
<a href="casera://add-property" style="display: inline-block; background: linear-gradient(135deg, #0079FF 0%%, #14B8A6 100%%); color: #FFFFFF; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; text-decoration: none; padding: 16px 32px; border-radius: 12px;">Add Your First Property</a>
</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;">Just open the Casera app and tap the + button to get started. It only takes a minute!</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>
%s
</td>
</tr>
%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(`<img src="%s/api/track/open/%s" width="1" height="1" style="display:none;" alt="" />`, baseURL, trackingID)
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;">Great job adding your property to Casera! Now it's time to set up your first maintenance task and start tracking your home care.</p>
<!-- Benefits 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;">Task ideas to get you started:</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127777;&#65039; <strong>HVAC Filter Replacement</strong> - Monthly or quarterly</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128167; <strong>Water Heater Flush</strong> - Annually</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127807; <strong>Lawn Care</strong> - Weekly or bi-weekly</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128374; <strong>Gutter Cleaning</strong> - Seasonal</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128293; <strong>Smoke Detector Test</strong> - Monthly</p>
</td>
</tr>
</table>
<!-- CTA Button -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 32px 0;">
<tr>
<td style="text-align: center;">
<a href="casera://add-task" style="display: inline-block; background: linear-gradient(135deg, #0079FF 0%%, #14B8A6 100%%); color: #FFFFFF; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; text-decoration: none; padding: 16px 32px; border-radius: 12px;">Create Your First Task</a>
</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;">Set up recurring tasks and Casera will remind you when they're due. No more forgotten maintenance!</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>
%s
</td>
</tr>
%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
}