Add html_body field for multipart email notifications

Callers can now supply a plain-text Body alongside an HTML html_body;
the SMTP sender emits multipart/alternative using both verbatim instead
of auto-stripping HTML to derive the plain-text fallback. The legacy
content_type=HTML path is preserved (deprecated) for existing callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 23:59:12 -07:00
parent a35b3e6283
commit ac3ba35736
5 changed files with 42 additions and 25 deletions
+7 -2
View File
@@ -68,8 +68,13 @@ type Notification struct {
// Body is the main content of the notification
Body string `json:"body"`
// ContentType specifies the format of the body (text or html)
// Defaults to "text" if not specified. HTML is auto-detected if body starts with < or contains HTML tags.
// HTMLBody is an optional HTML body for email notifications. If non-empty, the email is
// sent as multipart/alternative with Body as text/plain and HTMLBody as text/html.
// Ignored for non-email notification types.
HTMLBody string `json:"html_body,omitempty"`
// ContentType specifies the format of the body (text or html).
// Deprecated: prefer setting HTMLBody alongside a plain-text Body.
ContentType ContentType `json:"content_type,omitempty"`
// Recipients contains the target addresses (email, slack channel, ntfy topic, etc.)
+24 -20
View File
@@ -145,18 +145,15 @@ func (s *SMTPNotifier) buildMessage(notification *domain.Notification) string {
builder.WriteString(fmt.Sprintf("Subject: %s\r\n", notification.Subject))
builder.WriteString("MIME-Version: 1.0\r\n")
// Auto-detect HTML if content type not explicitly set to text
contentType := notification.ContentType
if contentType == "" || contentType == "auto" {
contentType = detectContentType(notification.Body)
}
// Build message based on content type
if contentType == domain.ContentTypeHTML {
// Send multipart/alternative with both text and HTML
s.buildMultipartMessage(&builder, notification)
} else {
// Send plain text only
switch {
case notification.HTMLBody != "":
// Caller provided distinct plain-text and HTML versions: send multipart/alternative
// using Body verbatim as text/plain and HTMLBody as text/html (no auto-strip).
s.buildMultipartMessage(&builder, notification.Body, notification.HTMLBody)
case isHTMLContent(notification):
// Legacy path (deprecated): Body itself is HTML. Auto-derive a plain-text fallback.
s.buildMultipartMessage(&builder, htmlToPlainText(notification.Body), notification.Body)
default:
builder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
builder.WriteString("\r\n")
builder.WriteString(notification.Body)
@@ -165,31 +162,38 @@ func (s *SMTPNotifier) buildMessage(notification *domain.Notification) string {
return builder.String()
}
// buildMultipartMessage builds a multipart/alternative email with both text and HTML versions
func (s *SMTPNotifier) buildMultipartMessage(builder *strings.Builder, notification *domain.Notification) {
// Generate a unique boundary
// isHTMLContent reports whether the notification's Body should be treated as HTML
// under the legacy content_type path.
func isHTMLContent(notification *domain.Notification) bool {
contentType := notification.ContentType
if contentType == "" || contentType == "auto" {
contentType = detectContentType(notification.Body)
}
return contentType == domain.ContentTypeHTML
}
// buildMultipartMessage builds a multipart/alternative email with the given plain-text
// and HTML parts.
func (s *SMTPNotifier) buildMultipartMessage(builder *strings.Builder, plainText, htmlBody string) {
boundary := generateBoundary()
builder.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary))
builder.WriteString("\r\n")
// Plain text version (auto-generated from HTML)
builder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
builder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
builder.WriteString("Content-Transfer-Encoding: 7bit\r\n")
builder.WriteString("\r\n")
builder.WriteString(htmlToPlainText(notification.Body))
builder.WriteString(plainText)
builder.WriteString("\r\n\r\n")
// HTML version
builder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
builder.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
builder.WriteString("Content-Transfer-Encoding: 7bit\r\n")
builder.WriteString("\r\n")
builder.WriteString(notification.Body)
builder.WriteString(htmlBody)
builder.WriteString("\r\n\r\n")
// End boundary
builder.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
}