aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-11 12:51:54 -0500
committerCraig Jennings <c@cjennings.net>2026-07-11 12:51:54 -0500
commit559530a208f8287d450cd842bfbdda92da97971c (patch)
treeab263ca6436d147c7a419f6cd622e5e89ee53d18
parent19a9377306ed71e28fc0dc71ad91975e52ef51a9 (diff)
downloaddotemacs-559530a208f8287d450cd842bfbdda92da97971c.tar.gz
dotemacs-559530a208f8287d450cd842bfbdda92da97971c.zip
fix(custom-buffer-file): stop leaking the MIME handle in the email viewer
cj/view-email-in-buffer dissected the message into a handle, then signaled a user-error when it found no displayable part, before the mm-destroy-parts cleanup. The handle leaked on every image-only or attachment-only email. Wrap the body in unwind-protect so the handle is always destroyed. While here, the eager top-level require of mm-decode only fed the mm-handle-type macro at compile time. The viewer already requires mm-decode at runtime before any mm-* call, so I moved it to eval-when-compile plus declare-function (matching the ps-print block above). Opening Emacs no longer loads mm-decode unless you view an email.
-rw-r--r--modules/custom-buffer-file.el40
-rw-r--r--tests/test-custom-buffer-file--view-email-in-buffer.el20
2 files changed, 45 insertions, 15 deletions
diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el
index 25555b53..d8e97e27 100644
--- a/modules/custom-buffer-file.el
+++ b/modules/custom-buffer-file.el
@@ -51,8 +51,13 @@
(declare-function ps-print-buffer-with-faces "ps-print")
(declare-function ps-print-region-with-faces "ps-print")
-;; mm-decode for email viewing (mm-handle-type is a macro, needs early require)
-(require 'mm-decode)
+;; mm-handle-type is a macro used in `cj/--email-handle-is-type-p', so mm-decode
+;; is only needed at compile time here; `cj/view-email-in-buffer' requires it at
+;; runtime before any mm-* call, so the eager startup require is unnecessary.
+(eval-when-compile (require 'mm-decode))
+(declare-function mm-dissect-buffer "mm-decode")
+(declare-function mm-insert-part "mm-decode")
+(declare-function mm-destroy-parts "mm-decode")
(require 'external-open) ;; for cj/xdg-open, cj/open-this-file-with
(require 'system-lib) ;; cj/confirm-strong (overwrite confirms), used below
@@ -941,19 +946,24 @@ Signals an error if:
(let* ((handle (mm-dissect-buffer t))
(displayable-part (cj/--email-find-displayable-part handle))
(buffer-name (format "*Email: %s*" (file-name-nondirectory buffer-file-name))))
- (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 displayable-part)
- (goto-char (point-min))
- (when (cj/--email-handle-is-type-p displayable-part "text/html")
- (shr-render-region (point-min) (point-max)))
- (goto-char (point-min))
- (special-mode)))
- (mm-destroy-parts handle)
- (switch-to-buffer buffer-name)))
+ ;; `mm-dissect-buffer' allocates handles that must be freed even when we
+ ;; bail out early (no displayable part), so destroy them from the cleanup
+ ;; form rather than after the body.
+ (unwind-protect
+ (progn
+ (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 displayable-part)
+ (goto-char (point-min))
+ (when (cj/--email-handle-is-type-p displayable-part "text/html")
+ (shr-render-region (point-min) (point-max)))
+ (goto-char (point-min))
+ (special-mode)))
+ (switch-to-buffer buffer-name))
+ (mm-destroy-parts handle))))
;; --------------------------- Buffer And File Keymap --------------------------
diff --git a/tests/test-custom-buffer-file--view-email-in-buffer.el b/tests/test-custom-buffer-file--view-email-in-buffer.el
index 99e0e44d..b0209b78 100644
--- a/tests/test-custom-buffer-file--view-email-in-buffer.el
+++ b/tests/test-custom-buffer-file--view-email-in-buffer.el
@@ -13,6 +13,7 @@
;;; Code:
(require 'ert)
+(require 'cl-lib)
(require 'testutil-general)
(require 'custom-buffer-file)
@@ -233,5 +234,24 @@ Note: shr may insert newlines between words for wrapping."
(kill-buffer)))
(test-email--teardown)))
+(ert-deftest test-custom-buffer-file--view-email-no-displayable-destroys-handle ()
+ "Error: the MIME handle is destroyed even when no displayable part is found.
+The `user-error' fires before cleanup, so without `unwind-protect' the dissected
+handle leaks."
+ (test-email--setup)
+ (unwind-protect
+ (let ((eml-file (test-email--create-eml-file test-email--image-only))
+ (destroy-called nil))
+ (with-current-buffer (find-file-noselect eml-file)
+ (let ((real (symbol-function 'mm-destroy-parts)))
+ (cl-letf (((symbol-function 'mm-destroy-parts)
+ (lambda (handle)
+ (setq destroy-called t)
+ (funcall real handle))))
+ (should-error (cj/view-email-in-buffer) :type 'user-error)))
+ (should destroy-called)
+ (kill-buffer)))
+ (test-email--teardown)))
+
(provide 'test-custom-buffer-file--view-email-in-buffer)
;;; test-custom-buffer-file--view-email-in-buffer.el ends here