From b6cdc834263ec62129c1b66f543d2b05ec567915 Mon Sep 17 00:00:00 2001 From: FromSi Date: Sat, 16 May 2026 02:00:53 +0500 Subject: [PATCH] fix(clib): escape markdown fallback (#1290) ## What? Adds a safe HTML fallback for Markdown conversion failures. When `MarkdownToHTML` fails in either the cgo `md4c` path or the no-cgo `goldmark` path, it now logs the failure and returns escaped plain text wrapped in `
` instead of returning raw Markdown bytes.

Also adds a focused test for the fallback escaping behavior.

## Why?

Closes #571

Previously, failed Markdown conversion returned the original Markdown
bytes. The email viewer then tried to parse those raw bytes as HTML,
which could render poorly and treat HTML-looking plain text as markup.

The new fallback keeps failed conversions readable in the HTML viewer
while escaping unsafe HTML-like input.
---
 clib/markdown.go               |  8 ++++++--
 clib/markdown_fallback.go      |  7 +++++++
 clib/markdown_fallback_test.go | 25 +++++++++++++++++++++++++
 clib/markdown_nocgo.go         |  4 +++-
 4 files changed, 41 insertions(+), 3 deletions(-)
 create mode 100644 clib/markdown_fallback.go
 create mode 100644 clib/markdown_fallback_test.go

diff --git a/clib/markdown.go b/clib/markdown.go
index 78115a28a1aec0bb470343cd176e846246075b32..0e143bebfe0275762e08a6a09d2aa3dc0dffddf0 100644
--- a/clib/markdown.go
+++ b/clib/markdown.go
@@ -56,7 +56,10 @@ static char* md4c_to_html(const char* input, size_t input_len, size_t* out_len)
 }
 */
 import "C"
-import "unsafe"
+import (
+	"log"
+	"unsafe"
+)
 
 // MarkdownToHTML converts Markdown bytes to HTML using md4c (C).
 // This is significantly faster than goldmark for large documents.
@@ -71,7 +74,8 @@ func MarkdownToHTML(md []byte) []byte {
 	var outLen C.size_t
 	result := C.md4c_to_html((*C.char)(cInput), C.size_t(len(md)), &outLen)
 	if result == nil {
-		return md // fallback to original on failure
+		log.Printf("markdown: md4c_to_html failed, falling back to escaped plain-text HTML")
+		return markdownPlainTextHTML(md)
 	}
 	defer C.free(unsafe.Pointer(result))
 
diff --git a/clib/markdown_fallback.go b/clib/markdown_fallback.go
new file mode 100644
index 0000000000000000000000000000000000000000..1ebabdcc920a95ecfea1f1b8b0a6cfea9cc6af26
--- /dev/null
+++ b/clib/markdown_fallback.go
@@ -0,0 +1,7 @@
+package clib
+
+import "html"
+
+func markdownPlainTextHTML(md []byte) []byte {
+	return []byte("
" + html.EscapeString(string(md)) + "
") +} diff --git a/clib/markdown_fallback_test.go b/clib/markdown_fallback_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e7b6430af68e26b8e92ebfc0df126b91b9315460 --- /dev/null +++ b/clib/markdown_fallback_test.go @@ -0,0 +1,25 @@ +package clib + +import ( + "strings" + "testing" +) + +func TestMarkdownPlainTextHTMLFallbackEscapesInput(t *testing.T) { + input := []byte("# Heading\n\n\nliteral") + + got := string(markdownPlainTextHTML(input)) + + if !strings.HasPrefix(got, "
") || !strings.HasSuffix(got, "
") { + t.Fatalf("fallback should wrap content in pre tag, got %q", got) + } + if strings.Contains(got, "