From 2e499389fc7068d73b8249d97ee7e28c6b2a86c6 Mon Sep 17 00:00:00 2001 From: Ivan Uzkikh Date: Sat, 21 Feb 2026 00:57:04 +0100 Subject: [PATCH] fix(smtp): preserve
line breaks in HTML emails HTML-only emails (e.g. from Synology DSM 7.3 Task Scheduler) use
tags for line breaks. The existing implementation passed the raw HTML body to bluemonday with AddSpaceWhenStrippingTag(true), which replaced every tag including
with a space, causing all content to appear on a single unreadable line. Fix: convert
tags to newlines before stripping HTML, so line break semantics are preserved in the notification body. Resolves the gap noted in #690 ("very sub-par" HTML support). --- server/smtp_server.go | 4 ++++ server/smtp_server_test.go | 41 ++++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/server/smtp_server.go b/server/smtp_server.go index ee28efc2..02414d3b 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -33,6 +33,7 @@ var ( var ( onlySpacesRegex = regexp.MustCompile(`(?m)^\s+$`) consecutiveNewLinesRegex = regexp.MustCompile(`\n{3,}`) + htmlLineBreakRegex = regexp.MustCompile(`(?i)`) ) const ( @@ -327,6 +328,9 @@ func readHTMLMailBody(reader io.Reader, transferEncoding string) (string, error) if err != nil { return "", err } + // Convert
tags to newlines before stripping HTML, so that line breaks + // in HTML emails (e.g. from Synology DSM, and other appliances) are preserved. + body = htmlLineBreakRegex.ReplaceAllString(body, "\n") stripped := bluemonday. StrictPolicy(). AddSpaceWhenStrippingTag(true). diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go index d4178a40..8b614819 100644 --- a/server/smtp_server_test.go +++ b/server/smtp_server_test.go @@ -694,7 +694,8 @@ home automation setup Now the light is on If you don't want to receive this message anymore, stop the push - services in your FRITZ!Box . + services in your FRITZ!Box . + Here you can see the active push services: "System > Push Service". This mail has ben sent by your FRITZ!Box automatically.` @@ -1354,9 +1355,11 @@ Congratulations! You have successfully set up the email notification on Synology s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/synology", r.URL.Path) require.Equal(t, "[Synology NAS] Test Message from Litts_NAS", r.Header.Get("Title")) - actual := readAll(t, r.Body) - expected := `Congratulations! You have successfully set up the email notification on Synology_NAS. For further system configurations, please visit http://192.168.1.28:5000/, http://172.16.60.5:5000/. (If you cannot connect to the server, please contact the administrator.) From Synology_NAS` - require.Equal(t, expected, actual) + expected := "Congratulations! You have successfully set up the email notification on Synology_NAS.\n" + + "For further system configurations, please visit http://192.168.1.28:5000/, http://172.16.60.5:5000/.\n" + + "(If you cannot connect to the server, please contact the administrator.)\n\n" + + "From Synology_NAS" + require.Equal(t, expected, readAll(t, r.Body)) }) conf.SMTPServerDomain = "mydomain.me" conf.SMTPServerAddrPrefix = "" @@ -1365,6 +1368,36 @@ Congratulations! You have successfully set up the email notification on Synology writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") } +func TestSmtpBackend_HTMLEmail_BrTagsPreserved(t *testing.T) { + email := `EHLO example.com +MAIL FROM: nas@example.com +RCPT TO: ntfy-alerts@ntfy.sh +DATA +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 8bit +Subject: Task Scheduler: daily-backup + +Task Scheduler has completed a scheduled task.

Task: daily-backup
Start time: Mon, 01 Jan 2026 02:00:00 +0000
Stop time: Mon, 01 Jan 2024 02:03:00 +0000
Current status: 0 (Normal)
Standard output/error:
OK

From MyNAS +. +` + s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/alerts", r.URL.Path) + require.Equal(t, "Task Scheduler: daily-backup", r.Header.Get("Title")) + expected := "Task Scheduler has completed a scheduled task.\n\n" + + "Task: daily-backup\n" + + "Start time: Mon, 01 Jan 2026 02:00:00 +0000\n" + + "Stop time: Mon, 01 Jan 2024 02:03:00 +0000\n" + + "Current status: 0 (Normal)\n" + + "Standard output/error:\n" + + "OK\n\n" + + "From MyNAS" + require.Equal(t, expected, readAll(t, r.Body)) + }) + defer s.Close() + defer c.Close() + writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") +} + func TestSmtpBackend_PlaintextWithToken(t *testing.T) { email := `EHLO example.com MAIL FROM: phil@example.com