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:
@@ -66,6 +66,7 @@ func (h *NotifierHandler) SendNotification(ctx context.Context, req *pb.SendNoti
|
||||
Priority: domain.Priority(req.Priority),
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
HTMLBody: req.HtmlBody,
|
||||
ContentType: contentType,
|
||||
Recipients: req.Recipients,
|
||||
CC: req.Cc,
|
||||
@@ -368,6 +369,7 @@ func convertDomainToProtoNotification(notif *domain.Notification) *pb.Notificati
|
||||
Status: convertDomainToProtoStatus(notif.Status),
|
||||
Subject: notif.Subject,
|
||||
Body: notif.Body,
|
||||
HtmlBody: notif.HTMLBody,
|
||||
Recipients: notif.Recipients,
|
||||
Metadata: convertInterfaceMapToString(notif.Metadata),
|
||||
CreatedAt: timestamppb.New(notif.CreatedAt),
|
||||
|
||||
@@ -81,7 +81,8 @@ message Notification {
|
||||
NotificationStatus status = 5;
|
||||
string subject = 6;
|
||||
string body = 7;
|
||||
ContentType content_type = 18; // Format of the body (text or html)
|
||||
ContentType content_type = 18 [deprecated = true]; // Deprecated: use html_body instead. Format of the body (text or html).
|
||||
string html_body = 19; // Optional HTML body for email; if set, sends multipart/alternative with body as text/plain and html_body as text/html. Ignored for non-email types.
|
||||
repeated string recipients = 8;
|
||||
repeated string cc = 16; // Carbon copy recipients (email only)
|
||||
repeated string bcc = 17; // Blind carbon copy recipients (email only)
|
||||
@@ -111,13 +112,14 @@ message SendNotificationRequest {
|
||||
Priority priority = 3;
|
||||
string subject = 4;
|
||||
string body = 5;
|
||||
ContentType content_type = 12; // Format of the body (text or html) - auto-detected if not specified
|
||||
ContentType content_type = 12 [deprecated = true]; // Deprecated: use html_body instead. Format of the body (text or html) - auto-detected if not specified.
|
||||
repeated string recipients = 6;
|
||||
repeated string cc = 10; // Carbon copy recipients (email only)
|
||||
repeated string bcc = 11; // Blind carbon copy recipients (email only)
|
||||
map<string, string> metadata = 7;
|
||||
google.protobuf.Timestamp scheduled_for = 8;
|
||||
int32 max_retries = 9;
|
||||
string html_body = 13; // Optional HTML body for email; if set, sends multipart/alternative with body as text/plain and html_body as text/html. Ignored for non-email types.
|
||||
}
|
||||
|
||||
// SendNotificationResponse returns the result of sending a notification
|
||||
|
||||
+5
-1
@@ -16,7 +16,8 @@ type SendNotificationRequest struct {
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
ContentType string `json:"content_type,omitempty"` // "text" or "html" - auto-detected if not specified
|
||||
HTMLBody string `json:"html_body,omitempty"` // Optional HTML body for email; if set, sends multipart/alternative.
|
||||
ContentType string `json:"content_type,omitempty"` // Deprecated: prefer html_body. "text" or "html".
|
||||
Recipients []string `json:"recipients"`
|
||||
CC []string `json:"cc,omitempty"` // Carbon copy recipients (email only)
|
||||
BCC []string `json:"bcc,omitempty"` // Blind carbon copy recipients (email only)
|
||||
@@ -75,6 +76,7 @@ func (r *SendNotificationRequest) ToNotification() *domain.Notification {
|
||||
Status: domain.StatusPending,
|
||||
Subject: r.Subject,
|
||||
Body: r.Body,
|
||||
HTMLBody: r.HTMLBody,
|
||||
ContentType: contentType,
|
||||
Recipients: r.Recipients,
|
||||
CC: r.CC,
|
||||
@@ -111,6 +113,7 @@ type Notification struct {
|
||||
Status string `json:"status"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
HTMLBody string `json:"html_body,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
Recipients []string `json:"recipients"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
@@ -134,6 +137,7 @@ func NotificationFromDomain(n *domain.Notification) Notification {
|
||||
Status: string(n.Status),
|
||||
Subject: n.Subject,
|
||||
Body: n.Body,
|
||||
HTMLBody: n.HTMLBody,
|
||||
ContentType: string(n.ContentType),
|
||||
Recipients: n.Recipients,
|
||||
CC: n.CC,
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user