From 13e8dc3549d5eefc4cc39659771e343d7a80a1ec Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 4 Feb 2026 05:54:45 -0600 Subject: fix(email): improve MIME handle parsing for email viewer - Add helper functions for robust MIME structure navigation - cj/--email-handle-is-type-p: check handle content type - cj/--email-find-displayable-part: recursively find HTML/plain text - Handle both leaf handles and nested multipart structures - Add 9 unit tests covering normal, boundary, and error cases --- modules/custom-buffer-file.el | 51 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) (limited to 'modules/custom-buffer-file.el') diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el index 92022b18..2c999266 100644 --- a/modules/custom-buffer-file.el +++ b/modules/custom-buffer-file.el @@ -343,6 +343,47 @@ Signals an error if the buffer is not visiting a file." (eww-open-file buffer-file-name) (user-error "Buffer is not visiting a file"))) +(defun cj/--email-handle-is-type-p (handle type) + "Return non-nil if HANDLE is a MIME part of TYPE (e.g., \"text/html\"). +TYPE matching is a prefix match, so \"text/html\" matches +\"text/html; charset=utf-8\"." + (when (and handle (listp handle)) + (let ((content-type (mm-handle-type handle))) + (and content-type + (listp content-type) + (stringp (car content-type)) + (string-prefix-p type (car content-type)))))) + +(defun cj/--email-find-displayable-part (handle) + "Find a displayable part (text/html or text/plain) in HANDLE. +Prefers text/html over text/plain. HANDLE can be a leaf handle or +a multipart structure. Returns the handle for the displayable part, or nil." + (cond + ;; Leaf handle that's HTML + ((cj/--email-handle-is-type-p handle "text/html") + handle) + ;; Leaf handle that's plain text - save it but keep looking for HTML + ((cj/--email-handle-is-type-p handle "text/plain") + handle) + ;; Multipart - search children + ((and (listp handle) (listp (car handle))) + (let ((html-part nil) + (text-part nil)) + (dolist (part handle) + (when (listp part) + (let ((found (cj/--email-find-displayable-part part))) + (when found + (if (cj/--email-handle-is-type-p found "text/html") + (setq html-part found) + (unless html-part + (setq text-part found))))))) + (or html-part text-part))) + ;; Multipart container (string content-type as car) + ((and (listp handle) (stringp (car handle))) + ;; This is a multipart with type info - search the cdr + (cj/--email-find-displayable-part (cdr handle))) + (t nil))) + (defun cj/view-email-in-buffer () "Render an .eml email file with proper MIME decoding. @@ -364,18 +405,16 @@ Signals an error if: (require 'mm-decode) (require 'shr) (let* ((handle (mm-dissect-buffer t)) - (html-part (or (mm-find-part-by-type handle "text/html" nil t) - (mm-find-part-by-type handle "text/plain" nil t))) + (displayable-part (cj/--email-find-displayable-part handle)) (buffer-name (format "*Email: %s*" (file-name-nondirectory buffer-file-name)))) - (unless html-part + (unless displayable-part (user-error "No displayable content found in email")) (with-current-buffer (get-buffer-create buffer-name) (let ((inhibit-read-only t)) (erase-buffer) - (mm-insert-part html-part) + (mm-insert-part displayable-part) (goto-char (point-min)) - (when (and (mm-handle-type html-part) - (string-match-p "text/html" (car (mm-handle-type html-part)))) + (when (cj/--email-handle-is-type-p displayable-part "text/html") (shr-render-region (point-min) (point-max))) (goto-char (point-min)) (special-mode))) -- cgit v1.2.3