;;; jao-notmuch.el --- Extensions for notmuch -*- lexical-binding: t; -*- ;; Copyright (C) 2021, 2022, 2023, 2024 jao ;; Author: jao ;; Keywords: mail ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;; Extensions to vanilla notmuch, mostly for tree view ;;; require: (require 'outline) (require 'mm-decode) (require 'mm-view) (require 'notmuch) (require 'notmuch-tree) (require 'notmuch-show) ;;; targetting the displayed message from the tree view (defvar-local jao-notmuch--tree-buffer nil) (declare eww--url-at-point "eww") (defun jao-notmuch-goto-message-buffer (&optional and-click no-record) (interactive "P") (when (window-live-p notmuch-tree-message-window) (let ((b (current-buffer))) (select-window notmuch-tree-message-window no-record) (setq-local jao-notmuch--tree-buffer b) (or (not and-click) (cond ((eww--url-at-point) (shr-browse-url) 'url) ((button-at (point)) (push-button) 'button)))))) (defun jao-notmuch-tree-toggle-message () (interactive) (if (window-live-p notmuch-tree-message-window) (notmuch-tree-close-message-window) (notmuch-tree-show-message nil))) (defun jao-notmuch-tree--find-tree-buffer () (or jao-notmuch--tree-buffer (let ((mb (current-buffer))) (seq-find (lambda (b) (with-current-buffer b (and (derived-mode-p 'notmuch-tree-mode) (eq notmuch-tree-message-buffer mb)))) (buffer-list))))) (defun jao-notmuch-goto-tree-buffer (&optional no-record) (interactive) (setq jao-notmuch--tree-buffer (jao-notmuch-tree--find-tree-buffer)) (if (buffer-live-p jao-notmuch--tree-buffer) (pop-to-buffer jao-notmuch--tree-buffer nil no-record) (user-error "No tree view for this buffer"))) (defun jao-notmuch-tree--looking-at-message () (when-let* ((id (plist-get (notmuch-tree-get-message-properties) :id))) (equal (concat "*notmuch-id:" id "*") (buffer-name (window-buffer notmuch-tree-message-window))))) (defun jao-notmuch-tree-scroll-or-next () "Scroll or next message in forest or exit if none." (interactive) (if (not (jao-notmuch-tree--looking-at-message)) (jao-notmuch-tree-show-or-scroll t) (if (notmuch-tree-scroll-message-window) (notmuch-tree-outline-next) (when (not (window-live-p notmuch-tree-message-window)) (notmuch-tree-show-message nil))))) (defun jao-notmuch-tree-show-or-scroll (force) "Show current message, or scroll it if visible." (interactive "P") (if (and (not force) (window-live-p notmuch-tree-message-window) (jao-notmuch-tree--looking-at-message)) (scroll-other-window 1) (notmuch-tree-show-message nil))) (notmuch-tree--define-do-in-message-window jao-notmuch-tree-end-of-buffer end-of-buffer) (notmuch-tree--define-do-in-message-window jao-notmuch-tree-beginning-of-buffer beginning-of-buffer) (defun jao-notmuch--page-urls (res) (save-excursion (goto-char (point-min)) (let ((pos (point))) (while (setq pos (next-single-property-change pos 'w3m-href-anchor)) (when-let* ((url (get-text-property pos 'w3m-href-anchor))) (when (stringp url) (cl-pushnew url res :test #'string=)))) (seq-uniq res #'string=)))) (defun jao-notmuch-message-urls () (save-window-excursion (when (or (derived-mode-p 'notmuch-show-mode) (jao-notmuch-goto-message-buffer)) (jao-notmuch--page-urls (notmuch-show--gather-urls))))) (defun jao-notmuch-browse-urls (&optional external) (interactive "P") (if-let* ((urls (jao-notmuch-message-urls))) (funcall (if external browse-url-secondary-browser-function #'browse-url) (completing-read "Browse URL: " urls)) (message "No URLs in this message"))) ;;; navigating URLs (require 'ffap) (defun jao-notmuch-show-next-button () (interactive) (when (get-text-property (point) 'w3m-href-anchor) (goto-char (next-single-property-change (point) 'w3m-href-anchor))) (if-let* ((pos (next-single-property-change (point) 'w3m-href-anchor))) (goto-char pos) (or (forward-button 1 nil t t) (ffap-next-guess)))) (defun jao-notmuch-show-previous-button () (interactive) (if-let* ((pos (previous-single-property-change (point) 'w3m-href-anchor))) (goto-char (previous-single-property-change pos 'w3m-href-anchor)) (or (backward-button 1 nil t t) (ffap-next-guess t)))) (defun jao-notmuch-show-ret () (interactive) (when-let* ((url (or (get-text-property (point) 'w3m-href-anchor) (thing-at-point-url-at-point)))) (browse-url url))) ;;; toggling mime parts and images (defun jao-notmuch--toggle-mime () (save-excursion (goto-char (point-min)) (while (and (re-search-forward "^\\[ text/\\(html\\|plain\\) " nil t) (button-at (point))) (notmuch-show-toggle-part-invisibility)))) (defun jao-notmuch-toggle-mime-parts () (interactive) (when (jao-notmuch-goto-message-buffer nil t) (goto-char (point-min)) (jao-notmuch--toggle-mime) (jao-notmuch-goto-tree-buffer t))) (defun jao-notmuch--view-html () "Open the text/html part of the current message using `notmuch-show-view-part'." (interactive) (save-excursion (goto-char (prop-match-beginning (text-property-search-forward :notmuch-part "text/html" (lambda (value notmuch-part) (equal (plist-get notmuch-part :content-type) value))))) (notmuch-show-view-part))) (defvar-local jao-notmuch--showing-images nil) (defun jao-notmuch--shr-toggle-images () (let* ((b notmuch-tree-message-buffer) (show (not (buffer-local-value 'jao-notmuch--showing-images b))) (mm-text-html-renderer 'shr) (shr-blocked-images (unless show shr-blocked-images)) (shr-inhibit-images (not show)) (notmuch-show-text/html-blocked-images (unless show notmuch-show-text/html-blocked-images)) (notmuch-multipart/alternative-discouraged (if show '("text/plain") notmuch-multipart/alternative-discouraged))) (notmuch-tree-close-message-window) (notmuch-tree-show-message nil) (with-current-buffer notmuch-tree-message-buffer (setq jao-notmuch--showing-images show)))) (defun jao-notmuch-toggle-images () (interactive) (cond ((memq mm-text-html-renderer '(w3m jao-w3m-html-renderer)) (when (fboundp 'jao-notmuch--w3m-toggle-images) (jao-notmuch--w3m-toggle-images))) (window-system (jao-notmuch--shr-toggle-images)) (notmuch-tree-message-buffer (if nil ;;(fboundp 'jao-open-in-x-frame) (let ((w (get-buffer-window notmuch-tree-message-buffer))) (jao-open-in-x-frame (window-width w) (window-height w)) (jao-notmuch--shr-toggle-images) (delete-window)) (with-current-buffer notmuch-tree-message-buffer (jao-notmuch--view-html)))))) ;;; header line with thread message counts (defun jao-notmuch--looking-at-new-p (&optional p) (when-let* ((ts (if p (plist-get p :tags) (notmuch-show-get-tags)))) (or (member "unread" ts) (member "new" ts)))) (defsubst jao-notmuch-tree--first-p (&optional msg) (plist-get (or msg (notmuch-tree-get-message-properties)) :first)) (defun jao-notmuch--message-counts (tree-buffer &optional thread) (with-current-buffer tree-buffer (let ((cnt) (total 0) (match 0) (msg)) (save-excursion (if thread (while (and (not (jao-notmuch-tree--first-p)) (zerop (forward-line -1)))) (goto-char (point-min))) (while (and (setq msg (notmuch-tree-get-message-properties)) (or (not cnt) (not thread) (not (jao-notmuch-tree--first-p msg)))) (unless cnt (setq cnt 0)) (setq total (1+ total)) (when (plist-get msg :match) (setq match (1+ match))) (when (jao-notmuch--looking-at-new-p msg) (setq cnt (1+ cnt))) (forward-line 1))) (when cnt (list total match cnt))))) (defvar jao-notmuch-header-line-format "%Q [%N / %M / %T] %n / %m / %t") (defun jao-notmuch--format-counts (query total match new ttotal tmatch tnew) (format-spec jao-notmuch-header-line-format `((?Q . ,query) (?T . ,total) (?N . ,new) (?M . ,match) (?t . ,ttotal) (?n . ,tnew) (?m . ,tmatch)))) (defun jao-notmuch--format-header-line (tree-buffer buffer subject) (let* ((n (jao-notmuch--message-counts tree-buffer)) (nc (jao-notmuch--message-counts tree-buffer t))) (with-current-buffer buffer (when (derived-mode-p 'notmuch-show-mode) (let* ((nc (append (or n '(0 0 0)) (or nc '(0 0 0)))) (q (if (string= tree-buffer subject) "" tree-buffer)) (c (apply 'jao-notmuch--format-counts q nc)) (n (- (window-width) 2 (string-width subject) (string-width c))) (subject (if (< n 0) (substring subject 0 n) subject)) (n (if (< n 0) 2 (+ n 2)))) (concat (when window-system " ") subject (make-string n ? ) c)))))) (defun jao-notmuch-message-header-line (subject) (if-let* ((cb (buffer-name (current-buffer))) (tb (seq-find (lambda (b) (with-current-buffer b (and (derived-mode-p 'notmuch-tree-mode) b))) (buffer-list)))) `((:eval (jao-notmuch--format-header-line ,(buffer-name tb) ,cb ,subject))) (concat " " subject))) (defun jao-notmuch--format-lighter () (when (derived-mode-p 'notmuch-tree-mode) (let* ((n (jao-notmuch--message-counts (current-buffer))) (nc (jao-notmuch--message-counts (current-buffer) t)) (nc (append (or n '(0 0 0)) (or nc '(0 0 0))))) (apply 'jao-notmuch--format-counts "" nc)))) (define-minor-mode jao-notmuch-thread-info-mode "" :lighter (:eval (format " %s" (jao-notmuch--format-lighter)))) ;;; tagging (defsubst jao-notmuch--has-tag (tag) (member tag (notmuch-tree-get-tags))) (defun jao-notmuch-tree--tag (tags reverse) (let ((c (notmuch-tag-change-list tags reverse))) (notmuch-tree-tag c))) (defun jao-notmuch-tree-tag-thread (tags reverse full) (when full (notmuch-tree-thread-top)) (let ((c (notmuch-tag-change-list tags reverse)) (level (or (notmuch-tree-get-prop :level) 0)) (go t)) (while go (notmuch-tree-tag c) (forward-line) (setq go (> (or (notmuch-tree-get-prop :level) 0) level))) (when notmuch-tree-outline-mode (ignore-errors (outline-show-branches)) (notmuch-tree-outline-next)))) (defun jao-notmuch-tree--tag-and-next (tags reverse) (jao-notmuch-tree--tag tags reverse) (notmuch-tree-outline-next t)) (defun jao-notmuch-tree-toggle-delete () (interactive) (let ((undo (jao-notmuch--has-tag "deleted"))) (jao-notmuch-tree--tag-and-next '("+deleted" "-new" "-unread") undo))) (defun jao-notmuch-tree-toggle-delete-thread (full) (interactive "P") (let ((undo (jao-notmuch--has-tag "deleted"))) (jao-notmuch-tree-tag-thread '("+deleted" "-new" "-unread") undo full))) (defun jao-notmuch-tree-read-thread (full) (interactive "P") (jao-notmuch-tree-tag-thread '("-unread" "-new") nil full)) (defun jao-notmuch-tree-toggle-flag () (interactive) (let ((tags (if (jao-notmuch--has-tag "flagged") '("-flagged") '("-unread" "-new" "-deleted" "+flagged")))) (jao-notmuch-tree--tag-and-next tags nil))) (defun jao-notmuch-tree-toggle-spam () (interactive) (let ((tags (if (jao-notmuch--has-tag "spam") '("-spam") '("-unread" "-new" "+spam")))) (jao-notmuch-tree--tag-and-next tags nil))) (defun jao-notmuch-tree-reset-tags () (interactive) (let ((tags (plist-get (notmuch-tree-get-message-properties) :orig-tags))) (jao-notmuch-tree--tag tags nil))) ;;; fcc (defvar jao-notmuch-mua-reply-not-inherited '("attachment" "sent" "new" "bigml" "jao" "trove")) (defun jao-notmuch-mua--fcc-dirs () (let* ((otags (notmuch-show-get-tags)) (trove (or (seq-some (lambda (x) (and (member x otags) x)) '("hacking" "bills" "feeds" "jao")) "jao")) (tags (seq-difference otags jao-notmuch-mua-reply-not-inherited)) (tagstr (mapconcat (lambda (s) (concat "+" s)) tags " ")) (fcc (concat "trove/" trove " " tagstr " -new +sent +trove")) (fcc-dirs (assoc-delete-all ".*" (copy-alist notmuch-fcc-dirs)))) (append fcc-dirs `((".*" . ,fcc))))) (defun jao-notmuch-mua--inherited-fcc () (let* ((fn (notmuch-show-get-filename)) (dest (and (string-match ".*/var/mail/\\(.+?\\)/.+" fn) (match-string 1 fn))) (tags (seq-difference (notmuch-show-get-tags) '("attachment" "sent" "new" "flagged"))) (tagstr (mapconcat (lambda (s) (concat "+" s)) tags " ")) (fcc (concat dest " " tagstr " -new +sent +trove")) (fcc-dirs (assoc-delete-all ".*" (copy-alist notmuch-fcc-dirs)))) (append fcc-dirs `((".*" . ,fcc))))) (defun jao-notmuch-mua-new-reply (fun &rest args) (let ((notmuch-fcc-dirs (and (not (notmuch-show-get-header :List-Id)) (jao-notmuch-mua--inherited-fcc)))) (apply fun args))) (advice-add 'notmuch-mua-new-reply :around #'jao-notmuch-mua-new-reply) ;;; results formatters (defun jao-notmuch-cmp-tags (a b) (or (> (length a) (length b)) (string-lessp a b))) (defun jao-notmuch-format-tags (fmt msg) (let ((ts (thread-last (notmuch-tree-format-field "tags" "%s" msg) (split-string) ;; (seq-sort-by #'length #'<) (seq-sort #'jao-notmuch-cmp-tags)))) (format-spec fmt `((?s . ,(mapconcat #'identity ts " ")))))) (defun jao-notmuch-format-tree-and-subject (fmt msg) (let ((tr (notmuch-tree-format-field "tree" " %s" msg)) (sb (notmuch-tree-format-field "subject" " %s" msg))) (format-spec fmt `((?s . ,(concat tr sb)))))) (defun jao-notmuch-format-msg-ticks (mails-rx msg) (let ((headers (plist-get msg :headers))) (cond ((string-match-p mails-rx (or (plist-get headers :To) "")) (propertize " »" 'face 'notmuch-tree-match-tree-face)) ((string-match-p mails-rx (or (plist-get headers :Cc) "")) (propertize " ¬" 'face 'notmuch-tree-match-tree-face)) (t " ")))) (provide 'jao-notmuch) ;;; jao-notmuch.el ends here