]> git.notmuchmail.org Git - notmuch/blobdiff - emacs/notmuch-show.el
Add a U binding to switch to unthreaded from other views
[notmuch] / emacs / notmuch-show.el
index 16ff45adf82e06a843cceb78ac1155c27819dcf3..214e279fde2af643762f26b39138088e5e3217d6 100644 (file)
@@ -38,6 +38,7 @@
 (require 'notmuch-mua)
 (require 'notmuch-crypto)
 (require 'notmuch-print)
 (require 'notmuch-mua)
 (require 'notmuch-crypto)
 (require 'notmuch-print)
+(require 'notmuch-draft)
 
 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
 (declare-function notmuch-search-next-thread "notmuch" nil)
 
 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
 (declare-function notmuch-search-next-thread "notmuch" nil)
 (declare-function notmuch-count-attachments "notmuch" (mm-handle))
 (declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
 (declare-function notmuch-tree "notmuch-tree"
 (declare-function notmuch-count-attachments "notmuch" (mm-handle))
 (declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
 (declare-function notmuch-tree "notmuch-tree"
-                 (&optional query query-context target buffer-name open-target))
+                 (&optional query query-context target buffer-name open-target unthreaded))
 (declare-function notmuch-tree-get-message-properties "notmuch-tree" nil)
 (declare-function notmuch-tree-get-message-properties "notmuch-tree" nil)
+(declare-function notmuch-unthreaded
+                 (&optional query query-context target buffer-name open-target))
 (declare-function notmuch-read-query "notmuch" (prompt))
 (declare-function notmuch-read-query "notmuch" (prompt))
+(declare-function notmuch-draft-resume "notmuch-draft" (id))
 
 (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
   "Headers that should be shown in a message, in this order.
 
 (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
   "Headers that should be shown in a message, in this order.
@@ -181,7 +185,7 @@ each attachment handler is logged in buffers with names beginning
 24.3 to work.")
 
 (defcustom notmuch-show-stash-mlarchive-link-alist
 24.3 to work.")
 
 (defcustom notmuch-show-stash-mlarchive-link-alist
-  '(("Gmane" . "http://mid.gmane.org/")
+  '(("Gmane" . "https://mid.gmane.org/")
     ("MARC" . "https://marc.info/?i=")
     ("Mail Archive, The" . "https://mid.mail-archive.com/")
     ("LKML" . "https://lkml.kernel.org/r/")
     ("MARC" . "https://marc.info/?i=")
     ("Mail Archive, The" . "https://mid.mail-archive.com/")
     ("LKML" . "https://lkml.kernel.org/r/")
@@ -245,6 +249,19 @@ every user interaction with notmuch."
   :type 'function
   :group 'notmuch-show)
 
   :type 'function
   :group 'notmuch-show)
 
+(defcustom notmuch-show-imenu-indent nil
+  "Should Imenu display messages indented.
+
+By default, Imenu (see Info node `(emacs) Imenu') in a
+notmuch-show buffer displays all messages straight.  This is
+because the default Emacs frontend for Imenu makes it difficult
+to select an Imenu entry with spaces in front.  Other imenu
+frontends such as counsel-imenu does not have this limitation.
+In these cases, Imenu entries can be indented to reflect the
+position of the message in the thread."
+  :type 'boolean
+  :group 'notmuch-show)
+
 (defmacro with-current-notmuch-show-message (&rest body)
   "Evaluate body with current buffer set to the text of current message"
   `(save-excursion
 (defmacro with-current-notmuch-show-message (&rest body)
   "Evaluate body with current buffer set to the text of current message"
   `(save-excursion
@@ -328,7 +345,7 @@ operation on the contents of the current buffer."
     (with-temp-buffer
       (insert all)
       (if indenting
     (with-temp-buffer
       (insert all)
       (if indenting
-         (indent-rigidly (point-min) (point-max) (- depth)))
+         (indent-rigidly (point-min) (point-max) (- (* notmuch-show-indent-messages-width depth))))
       ;; Remove the original header.
       (goto-char (point-min))
       (re-search-forward "^$" (point-max) nil)
       ;; Remove the original header.
       (goto-char (point-min))
       (re-search-forward "^$" (point-max) nil)
@@ -589,7 +606,7 @@ will return nil if the CID is unknown or cannot be retrieved."
     (unless (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)
       (push (cons 'notmuch-show-mode #'notmuch-show--cid-w3m-retrieve)
            w3m-cid-retrieve-function-alist)))
     (unless (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)
       (push (cons 'notmuch-show-mode #'notmuch-show--cid-w3m-retrieve)
            w3m-cid-retrieve-function-alist)))
-  (setq mm-inline-text-html-with-images t))
+  (setq mm-html-inhibit-images nil))
 
 (defvar w3m-current-buffer) ;; From `w3m.el'.
 (defun notmuch-show--cid-w3m-retrieve (url &rest args)
 
 (defvar w3m-current-buffer) ;; From `w3m.el'.
 (defun notmuch-show--cid-w3m-retrieve (url &rest args)
@@ -758,6 +775,20 @@ will return nil if the CID is unknown or cannot be retrieved."
 (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth button)
   (notmuch-show-insert-part-text/calendar msg part content-type nth depth button))
 
 (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth button)
   (notmuch-show-insert-part-text/calendar msg part content-type nth depth button))
 
+(if (version< emacs-version "25.3")
+    ;; https://bugs.gnu.org/28350
+    ;;
+    ;; For newer emacs, we fall back to notmuch-show-insert-part-*/*
+    ;; (see notmuch-show-handlers-for)
+    (defun notmuch-show-insert-part-text/enriched (msg part content-type nth depth button)
+      ;; By requiring enriched below, we ensure that the function enriched-decode-display-prop
+      ;; is defined before it will be shadowed by the letf below. Otherwise the version
+      ;; in enriched.el may be loaded a bit later and used instead (for the first time).
+      (require 'enriched)
+      (letf (((symbol-function 'enriched-decode-display-prop)
+                (lambda (start end &optional param) (list start end))))
+       (notmuch-show-insert-part-*/* msg part content-type nth depth button))))
+
 (defun notmuch-show-get-mime-type-of-application/octet-stream (part)
   ;; If we can deduce a MIME type from the filename of the attachment,
   ;; we return that.
 (defun notmuch-show-get-mime-type-of-application/octet-stream (part)
   ;; If we can deduce a MIME type from the filename of the attachment,
   ;; we return that.
@@ -907,7 +938,7 @@ will return nil if the CID is unknown or cannot be retrieved."
        (narrow-to-region part-beg part-end)
        (delete-region part-beg part-end)
        (apply #'notmuch-show-insert-bodypart-internal part-args)
        (narrow-to-region part-beg part-end)
        (delete-region part-beg part-end)
        (apply #'notmuch-show-insert-bodypart-internal part-args)
-       (indent-rigidly part-beg part-end depth))
+       (indent-rigidly part-beg part-end (* notmuch-show-indent-messages-width depth)))
       (goto-char part-end)
       (delete-char 1)
       (notmuch-show-record-part-information (second part-args)
       (goto-char part-end)
       (delete-char 1)
       (notmuch-show-record-part-information (second part-args)
@@ -1225,7 +1256,15 @@ matched."
   (interactive "sNotmuch show: \nP")
   (let ((buffer-name (generate-new-buffer-name
                      (or buffer-name
   (interactive "sNotmuch show: \nP")
   (let ((buffer-name (generate-new-buffer-name
                      (or buffer-name
-                         (concat "*notmuch-" thread-id "*")))))
+                         (concat "*notmuch-" thread-id "*"))))
+       ;; We override mm-inline-override-types to stop application/*
+       ;; parts from being displayed unless the user has customized
+       ;; it themselves.
+       (mm-inline-override-types
+        (if (equal mm-inline-override-types
+                   (eval (car (get 'mm-inline-override-types 'standard-value))))
+            (cons "application/*" mm-inline-override-types)
+          mm-inline-override-types)))
     (switch-to-buffer (get-buffer-create buffer-name))
     ;; No need to track undo information for this buffer.
     (setq buffer-undo-list t)
     (switch-to-buffer (get-buffer-create buffer-name))
     ;; No need to track undo information for this buffer.
     (setq buffer-undo-list t)
@@ -1237,7 +1276,9 @@ matched."
     ;; aren't wiped out.
     (setq notmuch-show-thread-id thread-id
          notmuch-show-parent-buffer parent-buffer
     ;; aren't wiped out.
     (setq notmuch-show-thread-id thread-id
          notmuch-show-parent-buffer parent-buffer
-         notmuch-show-query-context query-context
+         notmuch-show-query-context (if (or (string= query-context "")
+                                            (string= query-context "*"))
+                                        nil query-context)
 
          notmuch-show-process-crypto notmuch-crypto-process-mime
          ;; If `elide-toggle', invert the default value.
 
          notmuch-show-process-crypto notmuch-crypto-process-mime
          ;; If `elide-toggle', invert the default value.
@@ -1263,6 +1304,18 @@ matched."
        (message "No messages matched the query!")
        nil))))
 
        (message "No messages matched the query!")
        nil))))
 
+(defun notmuch-show--build-queries (thread context)
+  "Return a list of queries to try for this search.
+
+THREAD and CONTEXT are both strings, though CONTEXT may be nil.
+When CONTEXT is not nil, the first query is the conjunction of it
+and THREAD.  The next query is THREAD alone, and serves as a
+fallback if the prior matches no messages."
+  (let (queries)
+    (push (list thread) queries)
+    (if context (push (list thread "and (" context ")") queries))
+    queries))
+
 (defun notmuch-show--build-buffer (&optional state)
   "Display messages matching the current buffer context.
 
 (defun notmuch-show--build-buffer (&optional state)
   "Display messages matching the current buffer context.
 
@@ -1270,25 +1323,20 @@ Apply the previously saved STATE if supplied, otherwise show the
 first relevant message.
 
 If no messages match the query return NIL."
 first relevant message.
 
 If no messages match the query return NIL."
-  (let* ((basic-args (list notmuch-show-thread-id))
-        (args (if notmuch-show-query-context
-                  (append (list "\'") basic-args
-                          (list "and (" notmuch-show-query-context ")\'"))
-                (append (list "\'") basic-args (list "\'"))))
-        (cli-args (cons "--exclude=false"
+  (let* ((cli-args (cons "--exclude=false"
                         (when notmuch-show-elide-non-matching-messages
                           (list "--entire-thread=false"))))
                         (when notmuch-show-elide-non-matching-messages
                           (list "--entire-thread=false"))))
-
-        (forest (or (notmuch-query-get-threads (append cli-args args))
-                    ;; If a query context reduced the number of
-                    ;; results to zero, try again without it.
-                    (and notmuch-show-query-context
-                         (notmuch-query-get-threads (append cli-args basic-args)))))
-
+        (queries (notmuch-show--build-queries
+                  notmuch-show-thread-id notmuch-show-query-context))
+        (forest nil)
         ;; Must be reset every time we are going to start inserting
         ;; messages into the buffer.
         (notmuch-show-previous-subject ""))
         ;; Must be reset every time we are going to start inserting
         ;; messages into the buffer.
         (notmuch-show-previous-subject ""))
-
+    ;; Use results from the first query that returns some.
+    (while (and (not forest) queries)
+      (setq forest (notmuch-query-get-threads
+                   (append cli-args (list "'") (car queries) (list "'"))))
+      (setq queries (cdr queries)))
     (when forest
       (notmuch-show-insert-forest forest)
 
     (when forest
       (notmuch-show-insert-forest forest)
 
@@ -1425,6 +1473,7 @@ reset based on the original query."
   (let ((map (make-sparse-keymap)))
     (set-keymap-parent map notmuch-common-keymap)
     (define-key map "Z" 'notmuch-tree-from-show-current-query)
   (let ((map (make-sparse-keymap)))
     (set-keymap-parent map notmuch-common-keymap)
     (define-key map "Z" 'notmuch-tree-from-show-current-query)
+    (define-key map "U" 'notmuch-unthreaded-from-show-current-query)
     (define-key map (kbd "<C-tab>") 'widget-backward)
     (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
     (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
     (define-key map (kbd "<C-tab>") 'widget-backward)
     (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
     (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
@@ -1438,6 +1487,7 @@ reset based on the original query."
     (define-key map "|" 'notmuch-show-pipe-message)
     (define-key map "w" 'notmuch-show-save-attachments)
     (define-key map "V" 'notmuch-show-view-raw-message)
     (define-key map "|" 'notmuch-show-pipe-message)
     (define-key map "w" 'notmuch-show-save-attachments)
     (define-key map "V" 'notmuch-show-view-raw-message)
+    (define-key map "e" 'notmuch-show-resume-message)
     (define-key map "c" 'notmuch-show-stash-map)
     (define-key map "h" 'notmuch-show-toggle-visibility-headers)
     (define-key map "k" 'notmuch-tag-jump)
     (define-key map "c" 'notmuch-show-stash-map)
     (define-key map "h" 'notmuch-show-toggle-visibility-headers)
     (define-key map "k" 'notmuch-tag-jump)
@@ -1464,6 +1514,7 @@ reset based on the original query."
     (define-key map "<" 'notmuch-show-toggle-thread-indentation)
     (define-key map "t" 'toggle-truncate-lines)
     (define-key map "." 'notmuch-show-part-map)
     (define-key map "<" 'notmuch-show-toggle-thread-indentation)
     (define-key map "t" 'toggle-truncate-lines)
     (define-key map "." 'notmuch-show-part-map)
+    (define-key map "B" 'notmuch-show-browse-urls)
     map)
   "Keymap for \"notmuch show\" buffers.")
 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
     map)
   "Keymap for \"notmuch show\" buffers.")
 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
@@ -1498,7 +1549,11 @@ All currently available key bindings:
 \\{notmuch-show-mode-map}"
   (setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view)
   (setq buffer-read-only t
 \\{notmuch-show-mode-map}"
   (setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view)
   (setq buffer-read-only t
-       truncate-lines t))
+       truncate-lines t)
+  (setq imenu-prev-index-position-function
+        #'notmuch-show-imenu-prev-index-position-function)
+  (setq imenu-extract-index-name-function
+        #'notmuch-show-imenu-extract-index-name-function))
 
 (defun notmuch-tree-from-show-current-query ()
   "Call notmuch tree with the current query"
 
 (defun notmuch-tree-from-show-current-query ()
   "Call notmuch tree with the current query"
@@ -1507,6 +1562,13 @@ All currently available key bindings:
                notmuch-show-query-context
                (notmuch-show-get-message-id)))
 
                notmuch-show-query-context
                (notmuch-show-get-message-id)))
 
+(defun notmuch-unthreaded-from-show-current-query ()
+  "Call notmuch unthreaded with the current query"
+  (interactive)
+  (notmuch-unthreaded notmuch-show-thread-id
+                     notmuch-show-query-context
+                     (notmuch-show-get-message-id)))
+
 (defun notmuch-show-move-to-message-top ()
   (goto-char (notmuch-show-message-top)))
 
 (defun notmuch-show-move-to-message-top ()
   (goto-char (notmuch-show-message-top)))
 
@@ -1522,6 +1584,8 @@ All currently available key bindings:
 ;; region a->b is not found when point is at b. We walk backwards
 ;; until finding the property.
 (defun notmuch-show-message-extent ()
 ;; region a->b is not found when point is at b. We walk backwards
 ;; until finding the property.
 (defun notmuch-show-message-extent ()
+  "Return a cons cell containing the start and end buffer offset
+of the current message."
   (let (r)
     (save-excursion
       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
   (let (r)
     (save-excursion
       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
@@ -1642,9 +1706,10 @@ current thread."
 
 ;; dme: Would it make sense to use a macro for many of these?
 
 
 ;; dme: Would it make sense to use a macro for many of these?
 
+;; XXX TODO figure out what to do about multiple filenames
 (defun notmuch-show-get-filename ()
   "Return the filename of the current message."
 (defun notmuch-show-get-filename ()
   "Return the filename of the current message."
-  (notmuch-show-get-prop :filename))
+  (car (notmuch-show-get-prop :filename)))
 
 (defun notmuch-show-get-header (header &optional props)
   "Return the named header of the current message, if any."
 
 (defun notmuch-show-get-header (header &optional props)
   "Return the named header of the current message, if any."
@@ -1656,6 +1721,9 @@ current thread."
 (defun notmuch-show-get-date ()
   (notmuch-show-get-header :Date))
 
 (defun notmuch-show-get-date ()
   (notmuch-show-get-header :Date))
 
+(defun notmuch-show-get-timestamp ()
+  (notmuch-show-get-prop :timestamp))
+
 (defun notmuch-show-get-from ()
   (notmuch-show-get-header :From))
 
 (defun notmuch-show-get-from ()
   (notmuch-show-get-header :From))
 
@@ -1975,6 +2043,11 @@ to show, nil otherwise."
     (setq buffer-read-only t)
     (view-buffer buf 'kill-buffer-if-not-modified)))
 
     (setq buffer-read-only t)
     (view-buffer buf 'kill-buffer-if-not-modified)))
 
+(defun notmuch-show-resume-message ()
+  "Resume EDITING the current draft message."
+  (interactive)
+  (notmuch-draft-resume (notmuch-show-get-message-id)))
+
 (put 'notmuch-show-pipe-message 'notmuch-doc
      "Pipe the contents of the current message to a command.")
 (put 'notmuch-show-pipe-message 'notmuch-prefix-doc
 (put 'notmuch-show-pipe-message 'notmuch-doc
      "Pipe the contents of the current message to a command.")
 (put 'notmuch-show-pipe-message 'notmuch-prefix-doc
@@ -2193,7 +2266,7 @@ message will be \"unarchived\", i.e. the tag changes in
           (notmuch-tag-change-list notmuch-archive-tags unarchive))))
 
 (defun notmuch-show-archive-message-then-next-or-exit ()
           (notmuch-tag-change-list notmuch-archive-tags unarchive))))
 
 (defun notmuch-show-archive-message-then-next-or-exit ()
-  "Archive the current message, then show the next open message in the current thread.
+  "Archive current message, then show next open message in current thread.
 
 If at the last open message in the current thread, then exit back
 to search results."
 
 If at the last open message in the current thread, then exit back
 to search results."
@@ -2202,7 +2275,7 @@ to search results."
   (notmuch-show-next-open-message t))
 
 (defun notmuch-show-archive-message-then-next-or-next-thread ()
   (notmuch-show-next-open-message t))
 
 (defun notmuch-show-archive-message-then-next-or-next-thread ()
-  "Archive the current message, then show the next open message in the current thread.
+  "Archive current message, then show next open message in current or next thread.
 
 If at the last open message in the current thread, then show next
 thread from search."
 
 If at the last open message in the current thread, then show next
 thread from search."
@@ -2216,10 +2289,17 @@ thread from search."
   (interactive)
   (notmuch-common-do-stash (notmuch-show-get-cc)))
 
   (interactive)
   (notmuch-common-do-stash (notmuch-show-get-cc)))
 
-(defun notmuch-show-stash-date ()
-  "Copy date of current message to kill-ring."
-  (interactive)
-  (notmuch-common-do-stash (notmuch-show-get-date)))
+(put 'notmuch-show-stash-date 'notmuch-prefix-doc
+     "Copy timestamp of current message to kill-ring.")
+(defun notmuch-show-stash-date (&optional stash-timestamp)
+  "Copy date of current message to kill-ring.
+
+If invoked with a prefix argument, copy timestamp of current
+message to kill-ring."
+  (interactive "P")
+  (if stash-timestamp
+      (notmuch-common-do-stash (format "%d" (notmuch-show-get-timestamp)))
+    (notmuch-common-do-stash (notmuch-show-get-date))))
 
 (defun notmuch-show-stash-filename ()
   "Copy filename of current message to kill-ring."
 
 (defun notmuch-show-stash-filename ()
   "Copy filename of current message to kill-ring."
@@ -2432,6 +2512,56 @@ the new buffer."
                          (mailcap-mime-types) nil nil nil nil "text/plain")))
   (notmuch-show-apply-to-current-part-handle #'notmuch-show--mm-display-part mime-type))
 
                          (mailcap-mime-types) nil nil nil nil "text/plain")))
   (notmuch-show-apply-to-current-part-handle #'notmuch-show--mm-display-part mime-type))
 
+(defun notmuch-show-imenu-prev-index-position-function ()
+  "Move point to previous message in notmuch-show buffer.
+This function is used as a value for
+`imenu-prev-index-position-function'."
+  (if (bobp)
+      nil
+    (notmuch-show-previous-message)
+    t))
+
+(defun notmuch-show-imenu-extract-index-name-function ()
+  "Return imenu name for line at point.
+This function is used as a value for
+`imenu-extract-index-name-function'.  Point should be at the
+beginning of the line."
+  (back-to-indentation)
+  (buffer-substring-no-properties (if notmuch-show-imenu-indent
+                                     (line-beginning-position)
+                                   (point))
+                                 (line-end-position)))
+
+(defmacro notmuch-show--with-currently-shown-message (&rest body)
+  "Evaluate BODY with display restricted to the currently shown
+message."
+  `(save-excursion
+     (save-restriction
+      (let ((extent (notmuch-show-message-extent)))
+       (narrow-to-region (car extent) (cdr extent))
+       ,@body))))
+
+(defun notmuch-show--gather-urls ()
+  "Gather any URLs in the current message."
+  (notmuch-show--with-currently-shown-message
+   (let (urls)
+     (goto-char (point-min))
+     (while (re-search-forward goto-address-url-regexp (point-max) t)
+       (push (match-string-no-properties 0) urls))
+     (reverse urls))))
+
+(defun notmuch-show-browse-urls (&optional kill)
+  "Offer to browse any URLs in the current message.
+With a prefix argument, copy the URL to the kill ring rather than
+browsing."
+  (interactive "P")
+  (let ((urls (notmuch-show--gather-urls))
+       (prompt (if kill "Copy URL to kill ring: " "Browse URL: "))
+       (fn (if kill #'kill-new #'browse-url)))
+    (if urls
+       (funcall fn (completing-read prompt (cdr urls) nil nil (car urls)))
+      (message "No URLs found."))))
+
 (provide 'notmuch-show)
 
 ;;; notmuch-show.el ends here
 (provide 'notmuch-show)
 
 ;;; notmuch-show.el ends here