Include completion photos in task completed emails
- 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>
This commit is contained in:
@@ -54,6 +54,14 @@ type EmailAttachment struct {
|
||||
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()
|
||||
@@ -84,6 +92,39 @@ func (s *EmailService) SendEmailWithAttachment(to, subject, htmlBody, textBody s
|
||||
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>
|
||||
@@ -631,7 +672,8 @@ The Casera Team
|
||||
}
|
||||
|
||||
// SendTaskCompletedEmail sends an email notification when a task is completed
|
||||
func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, completedByName, residenceName string) error {
|
||||
// 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
|
||||
@@ -641,6 +683,39 @@ func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, comp
|
||||
|
||||
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 -->
|
||||
@@ -658,6 +733,7 @@ func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, comp
|
||||
</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>
|
||||
@@ -665,6 +741,7 @@ func (s *EmailService) SendTaskCompletedEmail(to, recipientName, taskTitle, comp
|
||||
%s`,
|
||||
emailHeader("Task Completed!"),
|
||||
name, residenceName, taskTitle, completedByName, completedTime,
|
||||
imagesHTML,
|
||||
emailFooter(time.Now().Year()))
|
||||
|
||||
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
|
||||
@@ -678,12 +755,16 @@ A task has been completed at %s:
|
||||
|
||||
Task: %s
|
||||
Completed by: %s
|
||||
Completed on: %s
|
||||
Completed on: %s%s
|
||||
|
||||
Best regards,
|
||||
The Casera Team
|
||||
`, name, residenceName, taskTitle, completedByName, completedTime)
|
||||
`, 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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user