Files
honeyDueAPI/internal/services/email_service.go
Trey t 7bd2cbabe9 Fix broken email icon by updating old domain references to myhoneydue.com
The email icon URL was pointing to honeyDue.treytartt.com which now returns 404.
Updated to api.myhoneydue.com along with BASE_URL, FROM_EMAIL, and CORS defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:38:55 -06:00

1130 lines
53 KiB
Go

package services
import (
"bytes"
"fmt"
"html/template"
"io"
"time"
"github.com/rs/zerolog/log"
"gopkg.in/gomail.v2"
"github.com/treytartt/honeydue-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
}
// ──────────────────────────────────────────────────────────────────────────────
// honeyDue "Warm Sage" Email Design System
// Matching the web landing page: myhoneydue.com
// ──────────────────────────────────────────────────────────────────────────────
// emailIconURL is the URL for the email icon
const emailIconURL = "https://api.myhoneydue.com/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 honeyDue 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;">&nbsp;</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="honeyDue" 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;">honeyDue</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;">&nbsp;</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;">&copy; %d honeyDue. All rights reserved.</p>
<p style="font-family: %s; font-size: 12px; margin: 8px 0 0 0;"><a href="mailto:honeydue@treymail.com" style="color: #6B8F71; text-decoration: none;">honeydue@treymail.com</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 honeyDue - 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 honeyDue 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 honeyDue!
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 honeyDue account, you can safely ignore this email.
- The honeyDue 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 honeyDue!"
name := firstName
if name == "" {
name = "there"
}
features := emailFeatureItem("&#127968;", "Manage Properties", "Track all your homes and rentals in one place", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#9989;", "Task Management", "Never miss maintenance with smart scheduling", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#128119;", "Contractor Directory", "Keep your trusted pros organized", "#FDF3EE", "#2D3436") +
emailFeatureItem("&#128196;", "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:honeydue@treymail.com" style="color: #6B8F71; text-decoration: none; font-weight: 600;">honeydue@treymail.com</a></p>
</td>
</tr>
</table>
%s`,
emailHeader("Welcome to honeyDue!"),
emailFontStack, name,
emailCalloutBox(features),
emailButton("Open honeyDue", "honeydue://home", "#C4856A"),
emailFontStack,
emailFooter(time.Now().Year()))
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
textBody := fmt.Sprintf(`Welcome to honeyDue!
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 honeydue@treymail.com
- The honeyDue 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 honeyDue!"
name := firstName
if name == "" {
name = "there"
}
features := emailFeatureItem("&#127968;", "Manage Properties", "Track all your homes and rentals in one place", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#9989;", "Task Management", "Never miss maintenance with smart scheduling", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#128119;", "Contractor Directory", "Keep your trusted pros organized", "#FDF3EE", "#2D3436") +
emailFeatureItem("&#128196;", "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:honeydue@treymail.com" style="color: #6B8F71; text-decoration: none; font-weight: 600;">honeydue@treymail.com</a></p>
</td>
</tr>
</table>
%s`,
emailHeader("Welcome to honeyDue!"),
emailFontStack, name,
emailCalloutBox(features),
emailButton("Open honeyDue", "honeydue://home", "#C4856A"),
emailFontStack,
emailFooter(time.Now().Year()))
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
textBody := fmt.Sprintf(`Welcome to honeyDue!
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 honeydue@treymail.com
- The honeyDue 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 honeyDue"
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 honeyDue:</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:honeydue@treymail.com" style="color: #6B8F71; text-decoration: none; font-weight: 600;">honeydue@treymail.com</a></p>
</td>
</tr>
</table>
%s`,
emailHeader("You're all set!"),
emailFontStack, name,
tips,
emailButton("Get Started", "honeydue://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 honeyDue:
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? honeydue@treymail.com
- The honeyDue 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 := "honeyDue - 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 honeyDue 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 := "honeyDue - 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 honeyDue 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 := "honeyDue - 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 honeydue@treymail.com 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 honeydue@treymail.com or reset your password right away.
- The honeyDue 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("honeyDue - 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;">&#10003;</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 honeyDue 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("honeyDue - 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 honeyDue", "honeydue://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 honeyDue 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 honeyDue - 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("&#127968;", "Track All Your Homes", "Manage single-family homes, apartments, or investment properties", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#128197;", "Never Miss Maintenance", "Set up recurring tasks with smart reminders", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#128196;", "Store Documents", "Keep warranties, manuals, and records in one place", "#FEF3C7", "#2D3436") +
emailFeatureItem("&#128119;", "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 honeyDue 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", "honeydue://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 honeyDue app and tap + to get started.
- The honeyDue 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 honeyDue"
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("&#127777;&#65039;", "HVAC Filter Replacement", "Monthly or quarterly", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#128167;", "Water Heater Flush", "Annually", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#127807;", "Lawn Care", "Weekly or bi-weekly", "#EDF2ED", "#2D3436") +
emailFeatureItem("&#128374;", "Gutter Cleaning", "Seasonal", "#FEF3C7", "#2D3436") +
emailFeatureItem("&#128293;", "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", "honeydue://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 honeyDue 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
}