Rewrote all 11 email templates to use the Casera web brand: Outfit font via Google Fonts, sage green (#6B8F71) brand stripe, cream (#FAFAF7) background, pill-shaped clay (#C4856A) CTA buttons, icon-badge feature cards, numbered tip cards, linen callout boxes, and refined light footer. Extracted reusable helpers (emailButton, emailCodeBox, emailCalloutBox, emailAlertBox, emailFeatureItem, emailTipCard) for consistent component composition. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1130 lines
52 KiB
Go
1130 lines
52 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
|
|
enabled bool
|
|
}
|
|
|
|
// NewEmailService creates a new email service
|
|
func NewEmailService(cfg *config.EmailConfig, enabled bool) *EmailService {
|
|
dialer := gomail.NewDialer(cfg.Host, cfg.Port, cfg.User, cfg.Password)
|
|
|
|
return &EmailService{
|
|
cfg: cfg,
|
|
dialer: dialer,
|
|
enabled: enabled,
|
|
}
|
|
}
|
|
|
|
// SendEmail sends an email
|
|
func (s *EmailService) SendEmail(to, subject, htmlBody, textBody string) error {
|
|
if !s.enabled {
|
|
log.Debug().Msg("Email sending disabled by feature flag")
|
|
return nil
|
|
}
|
|
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 {
|
|
if !s.enabled {
|
|
log.Debug().Msg("Email sending disabled by feature flag")
|
|
return nil
|
|
}
|
|
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 {
|
|
if !s.enabled {
|
|
log.Debug().Msg("Email sending disabled by feature flag")
|
|
return nil
|
|
}
|
|
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
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
// Casera "Warm Sage" Email Design System
|
|
// Matching the web landing page: casera.app
|
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
// emailIconURL is the URL for the email icon
|
|
const emailIconURL = "https://casera.app/images/icon.png"
|
|
|
|
// emailFontStack — Outfit via Google Fonts with progressive fallback
|
|
const emailFontStack = "Outfit, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
|
|
|
// baseEmailTemplate wraps all email content in the Casera shell:
|
|
// cream background → white card with warm shadow → sage brand stripe at top
|
|
func baseEmailTemplate() string {
|
|
return `<!DOCTYPE html>
|
|
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
<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">
|
|
<meta name="color-scheme" content="light">
|
|
<meta name="supported-color-schemes" content="light">
|
|
<title>%s</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<!--[if mso]>
|
|
<noscript>
|
|
<xml>
|
|
<o:OfficeDocumentSettings>
|
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
</o:OfficeDocumentSettings>
|
|
</xml>
|
|
</noscript>
|
|
<style>
|
|
table { border-collapse: collapse; }
|
|
td { font-family: Arial, sans-serif; }
|
|
</style>
|
|
<![endif]-->
|
|
<style>
|
|
/* Reset */
|
|
body, table, td, p, a, li { -webkit-text-size-adjust: 100%%; -ms-text-size-adjust: 100%%; }
|
|
img { -ms-interpolation-mode: bicubic; border: 0; outline: none; text-decoration: none; }
|
|
/* Mobile */
|
|
@media only screen and (max-width: 620px) {
|
|
.email-container { width: 100%% !important; max-width: 100%% !important; }
|
|
.email-body { padding: 32px 20px !important; }
|
|
.email-header { padding: 32px 20px !important; }
|
|
.stat-cell { display: block !important; width: 100%% !important; text-align: left !important; padding: 8px 0 !important; border-left: none !important; }
|
|
.feature-icon { width: 40px !important; height: 40px !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body style="margin: 0; padding: 0; background-color: #FAFAF7; font-family: ` + emailFontStack + `; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;">
|
|
<!-- Outer wrapper -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="background-color: #FAFAF7;">
|
|
<tr>
|
|
<td align="center" style="padding: 48px 24px;">
|
|
<!--[if mso]><table role="presentation" cellspacing="0" cellpadding="0" border="0" width="560"><tr><td><![endif]-->
|
|
<table role="presentation" class="email-container" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; margin: 0 auto;">
|
|
|
|
<!-- Sage brand stripe -->
|
|
<tr>
|
|
<td style="height: 4px; background-color: #6B8F71; border-radius: 16px 16px 0 0; font-size: 0; line-height: 0;"> </td>
|
|
</tr>
|
|
|
|
<!-- Main card -->
|
|
<tr>
|
|
<td style="background: #FFFFFF; border-left: 1px solid #E8E3DC; border-right: 1px solid #E8E3DC; border-bottom: 1px solid #E8E3DC; border-radius: 0 0 16px 16px; box-shadow: 0 4px 16px rgba(45, 52, 54, 0.06), 0 1px 4px rgba(45, 52, 54, 0.03);">
|
|
%s
|
|
</td>
|
|
</tr>
|
|
|
|
</table>
|
|
<!--[if mso]></td></tr></table><![endif]-->
|
|
|
|
<!-- Sub-footer (outside card) -->
|
|
<table role="presentation" class="email-container" width="560" cellspacing="0" cellpadding="0" border="0" style="max-width: 560px; margin: 0 auto;">
|
|
<tr>
|
|
<td style="padding: 24px 0 0 0; text-align: center;">
|
|
<p style="font-family: ` + emailFontStack + `; font-size: 12px; color: #8A8F87; margin: 0;">Your home, perfectly maintained.</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
// emailHeader renders the clean white header: logo + brand name + subtitle
|
|
func emailHeader(subtitle string) string {
|
|
return fmt.Sprintf(`
|
|
<!-- Header -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-header" style="padding: 40px 48px 32px 48px;">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td style="vertical-align: middle; padding-right: 14px;">
|
|
<img src="%s" alt="Casera" width="44" height="44" style="display: block; border-radius: 12px; border: 0;" />
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<p style="font-family: %s; font-size: 22px; font-weight: 800; color: #2D3436; margin: 0; letter-spacing: -0.5px;">Casera</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p style="font-family: %s; font-size: 26px; font-weight: 700; color: #2D3436; margin: 28px 0 0 0; letter-spacing: -0.5px; line-height: 1.2;">%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>`, emailIconURL, emailFontStack, emailFontStack, subtitle)
|
|
}
|
|
|
|
// emailDivider renders a subtle stone-colored horizontal rule
|
|
func emailDivider() string {
|
|
return `
|
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td style="padding: 0 48px;">
|
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td style="height: 1px; background-color: #E8E3DC; font-size: 0; line-height: 0;"> </td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>`
|
|
}
|
|
|
|
// emailFooter renders the footer with copyright
|
|
func emailFooter(year int) string {
|
|
return fmt.Sprintf(`
|
|
<!-- Divider -->
|
|
%s
|
|
<!-- Footer -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td style="padding: 24px 48px 32px 48px; text-align: center;">
|
|
<p style="font-family: %s; font-size: 12px; color: #8A8F87; margin: 0;">© %d Casera. All rights reserved.</p>
|
|
<p style="font-family: %s; font-size: 12px; margin: 8px 0 0 0;"><a href="mailto:support@casera.app" style="color: #6B8F71; text-decoration: none;">support@casera.app</a></p>
|
|
</td>
|
|
</tr>
|
|
</table>`, emailDivider(), emailFontStack, year, emailFontStack)
|
|
}
|
|
|
|
// emailButton renders a pill-shaped CTA button (matches landing page hero)
|
|
func emailButton(text, href, bgColor string) string {
|
|
return fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin: 32px 0 8px 0;">
|
|
<tr>
|
|
<td align="center">
|
|
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="%s" style="height:52px;v-text-anchor:middle;width:260px;" arcsize="50%%" fill="t" stroke="f"><v:fill type="tile" color="%s" /><v:textbox inset="0,0,0,0"><center><![endif]-->
|
|
<a href="%s" style="display: inline-block; background-color: %s; color: #FFFFFF; font-family: %s; font-size: 15px; font-weight: 600; text-decoration: none; padding: 14px 36px; border-radius: 50px; box-shadow: 0 2px 8px rgba(196, 133, 106, 0.25); mso-hide: all;">%s</a>
|
|
<!--[if mso]></center></v:textbox></v:roundrect><![endif]-->
|
|
</td>
|
|
</tr>
|
|
</table>`, href, bgColor, href, bgColor, emailFontStack, text)
|
|
}
|
|
|
|
// emailCodeBox renders a verification/reset code in a prominent box
|
|
func emailCodeBox(code, expiryText string) string {
|
|
return fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin: 24px 0;">
|
|
<tr>
|
|
<td align="center">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="background-color: #FAFAF7; border: 1px solid #E8E3DC; border-radius: 16px; min-width: 280px;">
|
|
<tr>
|
|
<td style="padding: 28px 40px; text-align: center;">
|
|
<p style="font-family: 'Courier New', Courier, monospace; font-size: 38px; font-weight: 700; letter-spacing: 10px; color: #6B8F71; margin: 0; line-height: 1;">%s</p>
|
|
<p style="font-family: %s; font-size: 13px; color: #8A8F87; margin: 14px 0 0 0;">%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>`, code, emailFontStack, expiryText)
|
|
}
|
|
|
|
// emailCalloutBox renders a linen-background callout with optional accent border
|
|
func emailCalloutBox(content string) string {
|
|
return fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin: 24px 0;">
|
|
<tr>
|
|
<td style="background-color: #F2EFE9; border-radius: 12px; padding: 24px 28px; border: 1px solid #E8E3DC;">
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>`, content)
|
|
}
|
|
|
|
// emailAlertBox renders a warning/info box with colored left accent
|
|
func emailAlertBox(title, body, accentColor, bgColor, titleColor string) string {
|
|
return fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin: 24px 0;">
|
|
<tr>
|
|
<td style="background-color: %s; border-left: 4px solid %s; border-radius: 0 12px 12px 0; padding: 18px 24px;">
|
|
<p style="font-family: %s; font-size: 14px; font-weight: 700; color: %s; margin: 0 0 6px 0;">%s</p>
|
|
<p style="font-family: %s; font-size: 14px; line-height: 1.6; color: #4B5563; margin: 0;">%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>`, bgColor, accentColor, emailFontStack, titleColor, title, emailFontStack, body)
|
|
}
|
|
|
|
// emailFeatureItem renders a single feature with an icon badge (matches landing page cards)
|
|
func emailFeatureItem(emoji, title, description, badgeBg, badgeColor string) string {
|
|
return fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin-bottom: 16px;">
|
|
<tr>
|
|
<td width="44" style="vertical-align: top; padding-right: 14px;">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="feature-icon" style="width: 44px; height: 44px; background-color: %s; border-radius: 10px; text-align: center; vertical-align: middle; font-size: 20px; line-height: 44px;">%s</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
<td style="vertical-align: top;">
|
|
<p style="font-family: %s; font-size: 14px; font-weight: 700; color: %s; margin: 0 0 4px 0;">%s</p>
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.5; color: #8A8F87; margin: 0;">%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>`, badgeBg, emoji, emailFontStack, badgeColor, title, emailFontStack, description)
|
|
}
|
|
|
|
// emailTipCard renders a numbered tip card with brand-colored accent (for post-verification)
|
|
func emailTipCard(number, title, description, accentColor, bgColor, titleColor string) string {
|
|
return fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin-bottom: 12px;">
|
|
<tr>
|
|
<td style="background-color: %s; border-radius: 12px; padding: 18px 22px; border: 1px solid %s;">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td width="32" style="vertical-align: top; padding-right: 14px;">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td style="width: 28px; height: 28px; background-color: %s; border-radius: 8px; text-align: center; vertical-align: middle;">
|
|
<p style="font-family: %s; font-size: 13px; font-weight: 800; color: #FFFFFF; margin: 0; line-height: 28px;">%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
<td style="vertical-align: top;">
|
|
<p style="font-family: %s; font-size: 15px; font-weight: 700; color: %s; margin: 0 0 4px 0;">%s</p>
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #4B5563; margin: 0;">%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>`, bgColor, bgColor, accentColor, emailFontStack, number, emailFontStack, titleColor, title, emailFontStack, description)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
// Email template methods
|
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
// 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 -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 4px 0;">Hi %s,</p>
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 0 0;">Welcome! Verify your email to get started:</p>
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 16px 0 0 0;">If you didn't create a Casera account, you can safely ignore this email.</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Verify your email"),
|
|
emailFontStack, name,
|
|
emailFontStack,
|
|
emailCodeBox(code, "Expires in 24 hours"),
|
|
emailFontStack,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Welcome to Casera!
|
|
|
|
Hi %s,
|
|
|
|
Welcome! Verify your email to get started.
|
|
|
|
Your verification code: %s
|
|
|
|
This code expires in 24 hours.
|
|
|
|
If you didn't create a Casera account, you can safely ignore this email.
|
|
|
|
- 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"
|
|
}
|
|
|
|
features := emailFeatureItem("🏠", "Manage Properties", "Track all your homes and rentals in one place", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("✅", "Task Management", "Never miss maintenance with smart scheduling", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("👷", "Contractor Directory", "Keep your trusted pros organized", "#FDF3EE", "#2D3436") +
|
|
emailFeatureItem("📄", "Document Storage", "Store warranties, manuals, and important records", "#FEF3C7", "#2D3436")
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 24px 0;">Hi %s, your account is ready! Here's what you can do:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 24px 0 0 0;">Questions? Reach out anytime at <a href="mailto:support@casera.app" style="color: #6B8F71; text-decoration: none; font-weight: 600;">support@casera.app</a></p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Welcome to Casera!"),
|
|
emailFontStack, name,
|
|
emailCalloutBox(features),
|
|
emailButton("Open Casera", "casera://home", "#C4856A"),
|
|
emailFontStack,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Welcome to Casera!
|
|
|
|
Hi %s,
|
|
|
|
Your account is ready! Here's what you can do:
|
|
|
|
- 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
|
|
|
|
Questions? Reach out anytime at support@casera.app
|
|
|
|
- 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"
|
|
}
|
|
|
|
features := emailFeatureItem("🏠", "Manage Properties", "Track all your homes and rentals in one place", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("✅", "Task Management", "Never miss maintenance with smart scheduling", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("👷", "Contractor Directory", "Keep your trusted pros organized", "#FDF3EE", "#2D3436") +
|
|
emailFeatureItem("📄", "Document Storage", "Store warranties, manuals, and important records", "#FEF3C7", "#2D3436")
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 24px 0;">Hi %s, your account is ready! Here's what you can do:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 24px 0 0 0;">Questions? Reach out anytime at <a href="mailto:support@casera.app" style="color: #6B8F71; text-decoration: none; font-weight: 600;">support@casera.app</a></p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Welcome to Casera!"),
|
|
emailFontStack, name,
|
|
emailCalloutBox(features),
|
|
emailButton("Open Casera", "casera://home", "#C4856A"),
|
|
emailFontStack,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Welcome to Casera!
|
|
|
|
Hi %s,
|
|
|
|
Your account is ready! Here's what you can do:
|
|
|
|
- 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
|
|
|
|
Questions? Reach out anytime at support@casera.app
|
|
|
|
- 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"
|
|
}
|
|
|
|
tips := emailTipCard("1", "Add Your Property", "Start by adding your home. You can manage multiple properties and share access with family.", "#6B8F71", "#EDF2ED", "#2D3436") +
|
|
emailTipCard("2", "Set Up Recurring Tasks", "Create tasks for HVAC filter changes, gutter cleaning, lawn care. We'll remind you when they're due.", "#C4856A", "#FDF3EE", "#2D3436") +
|
|
emailTipCard("3", "Track Maintenance History", "Complete tasks with notes and photos. Invaluable for warranty claims or when selling your home.", "#D97706", "#FEF3C7", "#2D3436") +
|
|
emailTipCard("4", "Store Important Documents", "Upload warranties, manuals, insurance policies. Find them instantly instead of digging through drawers.", "#6B8F71", "#F2EFE9", "#2D3436") +
|
|
emailTipCard("5", "Save Your Contractors", "Keep your trusted plumber, electrician, and other pros organized and one tap away.", "#C4856A", "#FDF3EE", "#2D3436")
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 24px 0;">Hi %s, your email is verified and you're ready to go! Here's how to get the most out of Casera:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 24px 0 0 0;">Questions or feedback? We'd love to hear from you at <a href="mailto:support@casera.app" style="color: #6B8F71; text-decoration: none; font-weight: 600;">support@casera.app</a></p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("You're all set!"),
|
|
emailFontStack, name,
|
|
tips,
|
|
emailButton("Get Started", "casera://home", "#C4856A"),
|
|
emailFontStack,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`You're All Set!
|
|
|
|
Hi %s,
|
|
|
|
Your email is verified and you're ready to go! Here's how to get the most out of Casera:
|
|
|
|
1. ADD YOUR PROPERTY
|
|
Start by adding your home. You can manage multiple properties and share access with family.
|
|
|
|
2. SET UP RECURRING TASKS
|
|
Create tasks for HVAC filter changes, gutter cleaning, lawn care. We'll remind you when they're due.
|
|
|
|
3. TRACK MAINTENANCE HISTORY
|
|
Complete tasks with notes and photos. Invaluable for warranty claims or when selling your home.
|
|
|
|
4. STORE IMPORTANT DOCUMENTS
|
|
Upload warranties, manuals, insurance policies. Find them instantly instead of digging through drawers.
|
|
|
|
5. SAVE YOUR CONTRACTORS
|
|
Keep your trusted plumber, electrician, and other pros organized and one tap away.
|
|
|
|
Questions? support@casera.app
|
|
|
|
- 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 -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 4px 0;">Hi %s,</p>
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0;">Here's your verification code:</p>
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 16px 0 0 0;">If you didn't request this, you can safely ignore this email.</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Verify your email"),
|
|
emailFontStack, name,
|
|
emailFontStack,
|
|
emailCodeBox(code, "Expires in 24 hours"),
|
|
emailFontStack,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Verify Your Email
|
|
|
|
Hi %s,
|
|
|
|
Here's your verification code: %s
|
|
|
|
This code expires in 24 hours.
|
|
|
|
If you didn't request this, you can safely ignore this email.
|
|
|
|
- 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 -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 4px 0;">Hi %s,</p>
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0;">Use this code to reset your password:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Reset your password"),
|
|
emailFontStack, name,
|
|
emailFontStack,
|
|
emailCodeBox(code, "Expires in 15 minutes"),
|
|
emailAlertBox("Didn't request this?", "If you didn't request a password reset, please ignore this email. Your password will remain unchanged.", "#C4856A", "#FDF3EE", "#A06B52"),
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Password Reset Request
|
|
|
|
Hi %s,
|
|
|
|
Use this code to reset your password: %s
|
|
|
|
This code expires in 15 minutes.
|
|
|
|
If you didn't request a password reset, please ignore this email. Your password will remain unchanged.
|
|
|
|
- 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 -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 4px 0;">Hi %s,</p>
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 0 0;">Your password was successfully changed on <strong style="color: #2D3436;">%s</strong>.</p>
|
|
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Password changed"),
|
|
emailFontStack, name,
|
|
emailFontStack, changeTime,
|
|
emailAlertBox("Wasn't you?", "If you didn't make this change, contact us immediately at support@casera.app or reset your password right away.", "#C4856A", "#FDF3EE", "#A06B52"),
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Password Changed
|
|
|
|
Hi %s,
|
|
|
|
Your password was successfully changed on %s.
|
|
|
|
If you didn't make this change, contact us immediately at support@casera.app or reset your password right away.
|
|
|
|
- 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 {
|
|
photoLabel := "Photo"
|
|
if len(images) > 1 {
|
|
photoLabel = "Photos"
|
|
}
|
|
imagesHTML = fmt.Sprintf(`
|
|
<p style="font-family: %s; font-size: 14px; font-weight: 600; color: #2D3436; margin: 24px 0 12px 0;">Completion %s</p>`, emailFontStack, photoLabel)
|
|
|
|
for i, img := range images {
|
|
imagesHTML += fmt.Sprintf(`
|
|
<img src="cid:%s" alt="Completion photo %d" style="max-width: 100%%; height: auto; border-radius: 12px; border: 1px solid #E8E3DC; margin-bottom: 8px;" />`, img.ContentID, i+1)
|
|
}
|
|
|
|
imagesText = fmt.Sprintf("\n\n[%d completion photo(s) attached]", len(images))
|
|
}
|
|
|
|
// Task detail card
|
|
taskCard := fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0" style="margin: 24px 0;">
|
|
<tr>
|
|
<td style="background-color: #EDF2ED; border-radius: 12px; padding: 24px 28px; border: 1px solid #EDF2ED;">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td width="32" style="vertical-align: top; padding-right: 14px;">
|
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td style="width: 28px; height: 28px; background-color: #6B8F71; border-radius: 50%%; text-align: center; vertical-align: middle;">
|
|
<p style="font-family: %s; font-size: 14px; color: #FFFFFF; margin: 0; line-height: 28px;">✓</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
<td style="vertical-align: top;">
|
|
<p style="font-family: %s; font-size: 17px; font-weight: 700; color: #2D3436; margin: 0 0 8px 0;">%s</p>
|
|
<p style="font-family: %s; font-size: 13px; color: #8A8F87; margin: 0; line-height: 1.6;">Completed by %s<br>%s</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>`,
|
|
emailFontStack,
|
|
emailFontStack, taskTitle,
|
|
emailFontStack, completedByName, completedTime,
|
|
imagesHTML)
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 0 0;">Hi %s, a task at <strong style="color: #2D3436;">%s</strong> has been completed:</p>
|
|
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Task completed!"),
|
|
emailFontStack, name, residenceName,
|
|
taskCard,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Task Completed!
|
|
|
|
Hi %s,
|
|
|
|
A task at %s has been completed:
|
|
|
|
Task: %s
|
|
Completed by: %s
|
|
Completed on: %s%s
|
|
|
|
- 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"
|
|
}
|
|
|
|
// Build stat cells
|
|
statRow := fmt.Sprintf(`
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="stat-cell" width="25%%" style="text-align: center; padding: 16px 8px;">
|
|
<p style="font-family: %s; font-size: 32px; font-weight: 800; color: #2D3436; margin: 0; line-height: 1;">%d</p>
|
|
<p style="font-family: %s; font-size: 11px; font-weight: 600; color: #8A8F87; text-transform: uppercase; letter-spacing: 1px; margin: 6px 0 0 0;">Total</p>
|
|
</td>
|
|
<td class="stat-cell" width="25%%" style="text-align: center; padding: 16px 8px; border-left: 1px solid #E8E3DC;">
|
|
<p style="font-family: %s; font-size: 32px; font-weight: 800; color: #6B8F71; margin: 0; line-height: 1;">%d</p>
|
|
<p style="font-family: %s; font-size: 11px; font-weight: 600; color: #8A8F87; text-transform: uppercase; letter-spacing: 1px; margin: 6px 0 0 0;">Done</p>
|
|
</td>
|
|
<td class="stat-cell" width="25%%" style="text-align: center; padding: 16px 8px; border-left: 1px solid #E8E3DC;">
|
|
<p style="font-family: %s; font-size: 32px; font-weight: 800; color: #D97706; margin: 0; line-height: 1;">%d</p>
|
|
<p style="font-family: %s; font-size: 11px; font-weight: 600; color: #8A8F87; text-transform: uppercase; letter-spacing: 1px; margin: 6px 0 0 0;">Pending</p>
|
|
</td>
|
|
<td class="stat-cell" width="25%%" style="text-align: center; padding: 16px 8px; border-left: 1px solid #E8E3DC;">
|
|
<p style="font-family: %s; font-size: 32px; font-weight: 800; color: #EF4444; margin: 0; line-height: 1;">%d</p>
|
|
<p style="font-family: %s; font-size: 11px; font-weight: 600; color: #8A8F87; text-transform: uppercase; letter-spacing: 1px; margin: 6px 0 0 0;">Overdue</p>
|
|
</td>
|
|
</tr>
|
|
</table>`,
|
|
emailFontStack, totalTasks,
|
|
emailFontStack,
|
|
emailFontStack, completed,
|
|
emailFontStack,
|
|
emailFontStack, pending,
|
|
emailFontStack,
|
|
emailFontStack, overdue,
|
|
emailFontStack)
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 24px 0;">Hi %s, here's your report for <strong style="color: #2D3436;">%s</strong>:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 14px; line-height: 1.6; color: #8A8F87; margin: 24px 0 0 0;">The full report is attached as a PDF.</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Tasks report"),
|
|
emailFontStack, name, residenceName,
|
|
emailCalloutBox(statRow),
|
|
emailButton("Open Casera", "casera://tasks", "#6B8F71"),
|
|
emailFontStack,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Tasks Report for %s
|
|
|
|
Hi %s,
|
|
|
|
Here's your report for %s:
|
|
|
|
Total: %d | Completed: %d | Pending: %d | Overdue: %d
|
|
|
|
The full report is attached as a PDF.
|
|
|
|
- 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)
|
|
|
|
features := emailFeatureItem("🏠", "Track All Your Homes", "Manage single-family homes, apartments, or investment properties", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("📅", "Never Miss Maintenance", "Set up recurring tasks with smart reminders", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("📄", "Store Documents", "Keep warranties, manuals, and records in one place", "#FEF3C7", "#2D3436") +
|
|
emailFeatureItem("👷", "Manage Contractors", "Keep your trusted pros organized and accessible", "#FDF3EE", "#2D3436")
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 24px 0;">Hi %s, you haven't added your first property yet. It only takes a minute and unlocks everything Casera has to offer:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 24px 0 0 0;">Open the app and tap + to get started.</p>
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Add your first property"),
|
|
emailFontStack, name,
|
|
emailCalloutBox(features),
|
|
emailButton("Add Your First Property", "casera://add-property", "#C4856A"),
|
|
emailFontStack,
|
|
trackingPixel,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Add Your First Property
|
|
|
|
Hi %s,
|
|
|
|
You haven't added your first property yet. It only takes a minute!
|
|
|
|
- Track all your homes
|
|
- Never miss maintenance
|
|
- Store important documents
|
|
- Manage contractors
|
|
|
|
Open the Casera app and tap + to get started.
|
|
|
|
- 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)
|
|
|
|
features := emailFeatureItem("🌡️", "HVAC Filter Replacement", "Monthly or quarterly", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("💧", "Water Heater Flush", "Annually", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("🌿", "Lawn Care", "Weekly or bi-weekly", "#EDF2ED", "#2D3436") +
|
|
emailFeatureItem("🕶", "Gutter Cleaning", "Seasonal", "#FEF3C7", "#2D3436") +
|
|
emailFeatureItem("🔥", "Smoke Detector Test", "Monthly", "#FDF3EE", "#2D3436")
|
|
|
|
bodyContent := fmt.Sprintf(`
|
|
%s
|
|
<!-- Body -->
|
|
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" border="0">
|
|
<tr>
|
|
<td class="email-body" style="padding: 0 48px 40px 48px;">
|
|
<p style="font-family: %s; font-size: 16px; line-height: 1.7; color: #4B5563; margin: 0 0 24px 0;">Hi %s, great job adding your property! Now set up your first task. Here are some ideas:</p>
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
<p style="font-family: %s; font-size: 13px; line-height: 1.6; color: #8A8F87; margin: 24px 0 0 0;">Set up recurring tasks and we'll remind you when they're due.</p>
|
|
%s
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
%s`,
|
|
emailHeader("Create your first task"),
|
|
emailFontStack, name,
|
|
emailCalloutBox(features),
|
|
emailButton("Create Your First Task", "casera://add-task", "#C4856A"),
|
|
emailFontStack,
|
|
trackingPixel,
|
|
emailFooter(time.Now().Year()))
|
|
|
|
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
|
|
|
textBody := fmt.Sprintf(`Create Your First Task
|
|
|
|
Hi %s,
|
|
|
|
Great job adding your property! Now set up your first task. Here are some ideas:
|
|
|
|
- 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 we'll remind you when they're due.
|
|
|
|
- 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
|
|
}
|