fix(smtp): preserve <br> line breaks in HTML emails

HTML-only emails (e.g. from Synology DSM 7.3 Task Scheduler) use <br>
tags for line breaks. The existing implementation passed the raw HTML
body to bluemonday with AddSpaceWhenStrippingTag(true), which replaced
every tag including <br> with a space, causing all content to appear
on a single unreadable line.

Fix: convert <br> 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).
This commit is contained in:
Ivan Uzkikh
2026-02-21 00:57:04 +01:00
parent 93cd7f99f8
commit 2e499389fc
2 changed files with 41 additions and 4 deletions

View File

@@ -33,6 +33,7 @@ var (
var ( var (
onlySpacesRegex = regexp.MustCompile(`(?m)^\s+$`) onlySpacesRegex = regexp.MustCompile(`(?m)^\s+$`)
consecutiveNewLinesRegex = regexp.MustCompile(`\n{3,}`) consecutiveNewLinesRegex = regexp.MustCompile(`\n{3,}`)
htmlLineBreakRegex = regexp.MustCompile(`(?i)<br\s*/?>`)
) )
const ( const (
@@ -327,6 +328,9 @@ func readHTMLMailBody(reader io.Reader, transferEncoding string) (string, error)
if err != nil { if err != nil {
return "", err return "", err
} }
// Convert <br> 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. stripped := bluemonday.
StrictPolicy(). StrictPolicy().
AddSpaceWhenStrippingTag(true). AddSpaceWhenStrippingTag(true).

View File

@@ -694,7 +694,8 @@ home automation setup
Now the light is on Now the light is on
If you don&#39;t want to receive this message anymore, stop the push If you don&#39;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: &#34;System &gt; Push Service&#34;. Here you can see the active push services: &#34;System &gt; Push Service&#34;.
This mail has ben sent by your FRITZ!Box automatically.` 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) { s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/synology", r.URL.Path) require.Equal(t, "/synology", r.URL.Path)
require.Equal(t, "[Synology NAS] Test Message from Litts_NAS", r.Header.Get("Title")) 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.\n" +
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` "For further system configurations, please visit http://192.168.1.28:5000/, http://172.16.60.5:5000/.\n" +
require.Equal(t, expected, actual) "(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.SMTPServerDomain = "mydomain.me"
conf.SMTPServerAddrPrefix = "" 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") 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.<BR><BR>Task: daily-backup<BR>Start time: Mon, 01 Jan 2026 02:00:00 +0000<BR>Stop time: Mon, 01 Jan 2024 02:03:00 +0000<BR>Current status: 0 (Normal)<BR>Standard output/error:<BR>OK<BR><BR>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) { func TestSmtpBackend_PlaintextWithToken(t *testing.T) {
email := `EHLO example.com email := `EHLO example.com
MAIL FROM: phil@example.com MAIL FROM: phil@example.com