Commit 43f0674679a7

Vincent Demeester <vincent@sbr.pm>
2026-01-29 12:10:22
feat(mu4e): enhance email capture templates with body summary
- Add vde-mu4e--extract-body-summary function to extract first 8 lines of email body, removing quotes, signatures, MIME headers, and HTML - Update mf/mr/mt capture templates to include: - Subject in heading - FROM and DATE in org properties - Body summary in quote block - Link preserved at bottom This ensures captured email tasks remain useful even if the original email is deleted, since all key context is stored in the org entry. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 159ee13
Changed files (1)
dots
.config
emacs
dots/.config/emacs/init.el
@@ -1912,21 +1912,52 @@ minibuffer, even without explicitly focusing it."
   (add-to-list 'org-capture-templates
 	       `("m" "✉ Email Workflow")
 	       t)
+  ;; Forward declaration - defined in mu4e config
+  (declare-function vde-mu4e--body-summary-for-capture "init")
   (add-to-list 'org-capture-templates
 	       `("mf" "Follow Up" entry
 		 (file ,org-inbox-file)
-		 "* TODO Follow up with %:from on %a\nSCHEDULED:%t\nDEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%i"
+		 "* TODO Follow up: %:subject :email:
+:PROPERTIES:
+:CREATED:\t%U
+:FROM:\t%:from
+:DATE:\t%:date
+:END:
+SCHEDULED:%t
+DEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))
+%(vde-mu4e--body-summary-for-capture)
+%i
+Link: %a"
 		 :immediate-finish t)
 	       t)
   (add-to-list 'org-capture-templates
 	       `("mr" "Read Later" entry
 		 (file ,org-inbox-file)
-		 "* TODO Read %:subject\nSCHEDULED:%t\nDEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))\n\n%a\n\n%i" :immediate-finish t)
+		 "* TODO Read: %:subject :email:
+:PROPERTIES:
+:CREATED:\t%U
+:FROM:\t%:from
+:DATE:\t%:date
+:END:
+SCHEDULED:%t
+DEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))
+%(vde-mu4e--body-summary-for-capture)
+%i
+Link: %a"
+		 :immediate-finish t)
 	       t)
   (add-to-list 'org-capture-templates
 	       `("mt" "Task from email" entry
 		 (file ,org-inbox-file)
-		 "* TODO %:subject :email:\n:PROPERTIES:\n:CREATED:\t%U\n:END:\n\nFrom: %:from\nEmail: %a\n\n%?"
+		 "* TODO %:subject :email:
+:PROPERTIES:
+:CREATED:\t%U
+:FROM:\t%:from
+:DATE:\t%:date
+:END:
+%(vde-mu4e--body-summary-for-capture)
+%?
+Link: %a"
 		 :empty-lines 1)
 	       t)
 
@@ -2216,6 +2247,78 @@ Add this function to the `after-save-hook'."
         message-sendmail-extra-arguments '("--read-envelope-from")
         message-send-mail-function 'message-send-mail-with-sendmail)
 
+  (defun vde-mu4e--extract-body-summary (&optional max-lines)
+    "Extract a summary from the current mu4e message body.
+Returns the first MAX-LINES lines (default 10) of the plain text body,
+with quoted lines and signatures removed."
+    (let ((max-lines (or max-lines 10))
+          (msg (mu4e-message-at-point 'noerror)))
+      (if (not msg)
+          ""
+        (let* ((path (mu4e-message-field msg :path))
+               (body-txt (when (and path (file-exists-p path))
+                           (with-temp-buffer
+                             (insert-file-contents path)
+                             (goto-char (point-min))
+                             ;; Skip headers - find empty line
+                             (when (re-search-forward "^$" nil t)
+                               (forward-line 1)
+                               (buffer-substring-no-properties (point) (point-max)))))))
+          (if (not body-txt)
+              ""
+            ;; Clean up the body
+            (with-temp-buffer
+              (insert body-txt)
+              ;; Try to decode if it looks like quoted-printable
+              (goto-char (point-min))
+              (when (re-search-forward "Content-Transfer-Encoding: quoted-printable" nil t)
+                (goto-char (point-min))
+                (when (re-search-forward "^$" nil t)
+                  (forward-line 1)
+                  (ignore-errors
+                    (quoted-printable-decode-region (point) (point-max)))))
+              ;; Remove MIME boundaries and headers
+              (goto-char (point-min))
+              (while (re-search-forward "^--.*$\\|^Content-Type:.*$\\|^Content-Transfer-Encoding:.*$" nil t)
+                (replace-match ""))
+              ;; Remove HTML tags if present
+              (goto-char (point-min))
+              (while (re-search-forward "<[^>]+>" nil t)
+                (replace-match ""))
+              ;; Remove quoted lines (starting with >)
+              (goto-char (point-min))
+              (while (re-search-forward "^>.*$" nil t)
+                (replace-match ""))
+              ;; Remove signature (everything after -- )
+              (goto-char (point-min))
+              (when (re-search-forward "^-- $" nil t)
+                (delete-region (match-beginning 0) (point-max)))
+              ;; Remove excessive whitespace
+              (goto-char (point-min))
+              (while (re-search-forward "^[ \t]*\n\\([ \t]*\n\\)+" nil t)
+                (replace-match "\n"))
+              ;; Get first N non-empty lines
+              (goto-char (point-min))
+              (let ((lines nil)
+                    (count 0))
+                (while (and (< count max-lines) (not (eobp)))
+                  (let ((line (string-trim (buffer-substring-no-properties
+                                            (line-beginning-position)
+                                            (line-end-position)))))
+                    (when (and (> (length line) 0)
+                               (not (string-match-p "^\\s-*$" line)))
+                      (push line lines)
+                      (setq count (1+ count))))
+                  (forward-line 1))
+                (string-join (nreverse lines) "\n"))))))))
+
+  (defun vde-mu4e--body-summary-for-capture ()
+    "Return email body summary for org-capture template."
+    (let ((summary (vde-mu4e--extract-body-summary 8)))
+      (if (string-empty-p summary)
+          ""
+        (concat "\n#+begin_quote\n" summary "\n#+end_quote"))))
+
   (defun vde-mu4e--mark-get-copy-target ()
     "Ask for a copy target, and propose to create it if it does not exist."
     (let* ((target (mu4e-ask-maildir "Copy message to: "))