- Add EmbeddedImage struct and SendEmailWithEmbeddedImages method for inline images - Update SendTaskCompletedEmail to accept and display completion photos - Read images from disk via StorageService and embed with Content-ID references - Wire StorageService to TaskService for image access - Photos display inline in HTML email body, works across all email clients 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1042 lines
58 KiB
Go
1042 lines
58 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
|
|
}
|
|
|
|
// EmbeddedImage represents an inline image in an email
|
|
type EmbeddedImage struct {
|
|
ContentID string // Used in HTML as src="cid:ContentID"
|
|
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
|
|
}
|
|
|
|
// SendEmailWithEmbeddedImages sends an email with inline embedded images
|
|
func (s *EmailService) SendEmailWithEmbeddedImages(to, subject, htmlBody, textBody string, images []EmbeddedImage) 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)
|
|
|
|
// Embed each image with Content-ID for inline display
|
|
for _, img := range images {
|
|
m.Embed(img.Filename,
|
|
gomail.SetCopyFunc(func(w io.Writer) error {
|
|
_, err := w.Write(img.Data)
|
|
return err
|
|
}),
|
|
gomail.SetHeader(map[string][]string{
|
|
"Content-Type": {img.ContentType},
|
|
"Content-ID": {"<" + img.ContentID + ">"},
|
|
"Content-Disposition": {"inline; filename=\"" + img.Filename + "\""},
|
|
}),
|
|
)
|
|
}
|
|
|
|
if err := s.dialer.DialAndSend(m); err != nil {
|
|
log.Error().Err(err).Str("to", to).Str("subject", subject).Int("images", len(images)).Msg("Failed to send email with embedded images")
|
|
return fmt.Errorf("failed to send email: %w", err)
|
|
}
|
|
|
|
log.Info().Str("to", to).Str("subject", subject).Int("images", len(images)).Msg("Email with embedded images 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;">© %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;">🏠 <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;">✅ <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;">👷 <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;">📄 <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)
|
|
}
|
|
|
|
// SendGoogleWelcomeEmail sends a welcome email for Google Sign In users (no verification needed)
|
|
func (s *EmailService) SendGoogleWelcomeEmail(to, firstName string) error {
|
|
subject := "Welcome to Casera!"
|
|
|
|
name := firstName
|
|
if name == "" {
|
|
name = "there"
|
|
}
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- 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;">🏠 <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;">✅ <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;">👷 <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;">📄 <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;">🏠 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;">📅 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;">📝 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;">📄 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;">👷 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
|
|
// images parameter is optional - pass nil or empty slice if no images
|
|
func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, completedByName, residenceName string, images []EmbeddedImage) 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")
|
|
|
|
// Build images HTML section if images are provided
|
|
imagesHTML := ""
|
|
imagesText := ""
|
|
if len(images) > 0 {
|
|
imagesHTML = `
|
|
<!-- Completion Photos Section -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-top: 24px;">
|
|
<tr>
|
|
<td>
|
|
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 600; color: #1a1a1a; margin: 0 0 12px 0;">Completion Photo` + func() string {
|
|
if len(images) > 1 {
|
|
return "s"
|
|
}
|
|
return ""
|
|
}() + `:</p>
|
|
</td>
|
|
</tr>`
|
|
|
|
for i, img := range images {
|
|
imagesHTML += fmt.Sprintf(`
|
|
<tr>
|
|
<td style="padding: 8px 0;">
|
|
<img src="cid:%s" alt="Completion photo %d" style="max-width: 100%%; height: auto; border-radius: 8px; border: 1px solid #E5E7EB;" />
|
|
</td>
|
|
</tr>`, img.ContentID, i+1)
|
|
}
|
|
|
|
imagesHTML += `
|
|
</table>`
|
|
|
|
imagesText = fmt.Sprintf("\n\n[%d completion photo(s) attached]", len(images))
|
|
}
|
|
|
|
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>
|
|
%s
|
|
|
|
<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,
|
|
imagesHTML,
|
|
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%s
|
|
|
|
Best regards,
|
|
The Casera Team
|
|
`, name, residenceName, taskTitle, completedByName, completedTime, imagesText)
|
|
|
|
// Use embedded images method if we have images, otherwise use simple send
|
|
if len(images) > 0 {
|
|
return s.SendEmailWithEmbeddedImages(to, subject, htmlBody, textBody, images)
|
|
}
|
|
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;">🏠 <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;">📅 <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;">📄 <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;">👷 <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;">🌡️ <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;">💧 <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;">🌿 <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;">🕶 <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;">🔥 <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
|
|
}
|