From 3447d76c93d1b2da564d7d196c95a03b24e870d7 Mon Sep 17 00:00:00 2001 From: Adam Miribyan <321133+adammiribyan@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:34:10 +0200 Subject: [PATCH] List the files inside an embedded text/html attachment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An HTML email from outside HEY arrives as one
with contentType "text/html" and the original markup in its content string (21751f9). When that email carried files -- an Outlook sender attaching two PDFs, say -- they sit inside that markup as elements, so `hey attachment list` answered "0 attachments" for a thread whose `hey thread read` plainly showed the 📎 lines, and `hey attachment save` had nothing to address. findAttachments now walks into the embedded markup the way findImages already does, bounded by embeddedContentDepthLimit, and lists what it finds there. The wrapper itself is still not listed: an embedded body is not a downloadable file, and the test that says so still holds. --- internal/htmlutil/htmlutil.go | 18 ++++++++++++++---- internal/htmlutil/htmlutil_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/internal/htmlutil/htmlutil.go b/internal/htmlutil/htmlutil.go index a012c501..e192dbb4 100644 --- a/internal/htmlutil/htmlutil.go +++ b/internal/htmlutil/htmlutil.go @@ -73,7 +73,7 @@ func ExtractAttachments(s string) []Attachment { return nil } var attachments []Attachment - findAttachments(doc, &attachments) + findAttachments(doc, &attachments, 0) return attachments } @@ -336,7 +336,7 @@ func parseEmbeddedContent(content string, depth int) *html.Node { return doc } -func findAttachments(n *html.Node, attachments *[]Attachment) { +func findAttachments(n *html.Node, attachments *[]Attachment, depth int) { if n.Type == html.ElementNode { switch n.Data { case "action-text-attachment": @@ -352,7 +352,10 @@ func findAttachments(n *html.Node, attachments *[]Attachment) { *attachments = append(*attachments, attachment) } case "figure": - if trix := parseTrixAttachment(n); trix != nil && trix.URL != "" && trix.Filename != "" { + trix := parseTrixAttachment(n) + switch { + case trix == nil: + case trix.URL != "" && trix.Filename != "": *attachments = append(*attachments, Attachment{ URL: trix.URL, Filename: trix.Filename, @@ -360,11 +363,18 @@ func findAttachments(n *html.Node, attachments *[]Attachment) { ByteSize: nonnegativeAttachmentByteSize(trix.Filesize), SGID: trix.SGID, }) + case trix.Content != "": + // An inbound email's files are inside the embedded markup, not + // on the figure that wraps it. The wrapper itself is not listed: + // an embedded body is not a downloadable file. + if doc := parseEmbeddedContent(trix.Content, depth); doc != nil { + findAttachments(doc, attachments, depth+1) + } } } } for child := n.FirstChild; child != nil; child = child.NextSibling { - findAttachments(child, attachments) + findAttachments(child, attachments, depth) } } diff --git a/internal/htmlutil/htmlutil_test.go b/internal/htmlutil/htmlutil_test.go index f5f703b9..c49bd118 100644 --- a/internal/htmlutil/htmlutil_test.go +++ b/internal/htmlutil/htmlutil_test.go @@ -194,6 +194,33 @@ func TestExtractAttachmentsSkipsEmbeddedHTMLAttachment(t *testing.T) { } } +func TestExtractAttachmentsInsideEmbeddedHTMLAttachment(t *testing.T) { + // An HTML email from outside HEY arrives as one text/html trix attachment + // whose content string holds the original markup, files included. + content := `
` + + attachments := ExtractAttachments(content) + if len(attachments) != 1 { + t.Fatalf("ExtractAttachments = %+v, want the file inside the embedded body", attachments) + } + got := attachments[0] + if got.Filename != "payslip.pdf" || got.URL != "/rails/blobs/payslip.pdf" || got.ContentType != "application/pdf" || got.SGID != "sgid-1" || got.ByteSize == nil || *got.ByteSize != 44218 { + t.Errorf("embedded attachment = %+v", got) + } +} + +func TestExtractAttachmentsEmbeddedContentStopsRecursing(t *testing.T) { + nested := `
` + for range embeddedContentDepthLimit + 2 { + nested = `
` + } + + if attachments := ExtractAttachments(nested); len(attachments) != 0 { + t.Errorf("ExtractAttachments = %+v, should stop before the innermost level", attachments) + } +} + func TestExtractAttachments(t *testing.T) { h := `
`