;
; Authors: Carl Worth <cworth@cworth.org>
+(require 'cl)
+(require 'mm-view)
+
(defvar notmuch-show-mode-map
(let ((map (make-sparse-keymap)))
; I don't actually want all of these toggle commands occupying
; toggling visibility of these components. Probably using
; overlays-at to query and manipulate the current overlay.
(define-key map "a" 'notmuch-show-archive-thread)
+ (define-key map "A" 'notmuch-show-mark-read-then-archive-thread)
(define-key map "b" 'notmuch-show-toggle-body-read-visible)
(define-key map "c" 'notmuch-show-toggle-citations-visible)
(define-key map "h" 'notmuch-show-toggle-headers-visible)
+ (define-key map "m" 'message-mail)
(define-key map "n" 'notmuch-show-next-message)
+ (define-key map "N" 'notmuch-show-mark-read-then-next-open-message)
(define-key map "p" 'notmuch-show-previous-message)
(define-key map (kbd "C-n") 'notmuch-show-next-line)
(define-key map (kbd "C-p") 'notmuch-show-previous-line)
(define-key map "q" 'kill-this-buffer)
+ (define-key map "r" 'notmuch-show-reply)
(define-key map "s" 'notmuch-show-toggle-signatures-visible)
+ (define-key map "v" 'notmuch-show-view-all-mime-parts)
(define-key map "w" 'notmuch-show-view-raw-message)
(define-key map "x" 'kill-this-buffer)
(define-key map "+" 'notmuch-show-add-tag)
(define-key map "-" 'notmuch-show-remove-tag)
(define-key map (kbd "DEL") 'notmuch-show-rewind)
(define-key map " " 'notmuch-show-advance-marking-read-and-archiving)
+ (define-key map "|" 'notmuch-show-pipe-message)
+ (define-key map "?" 'describe-mode)
map)
"Keymap for \"notmuch show\" buffers.")
(fset 'notmuch-show-mode-map notmuch-show-mode-map)
-(defvar notmuch-show-signature-lines-max 6
+(defvar notmuch-show-signature-regexp "\\(-- ?\\|_+\\)$"
+ "Pattern to match a line that separates content from signature.
+
+The regexp can (and should) include $ to match the end of the
+line, but should not include ^ to match the beginning of the
+line. This is because notmuch may have inserted additional space
+for indentation at the beginning of the line. But notmuch will
+move past the indentation when testing this pattern, (so that the
+pattern can still test against the entire line).")
+
+(defvar notmuch-show-signature-lines-max 12
"Maximum length of signature that will be hidden by default.")
(set 'notmuch-show-message-begin-regexp "\fmessage{")
(set 'notmuch-show-part-end-regexp "\fpart}")
(set 'notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
-(set 'notmuch-show-id-regexp "ID: \\([^ ]*\\)")
-(set 'notmuch-show-filename-regexp "Filename: \\(.*\\)$")
+(set 'notmuch-show-id-regexp "\\(id:[^ ]*\\)")
+(set 'notmuch-show-depth-regexp " depth:\\([0-9]*\\) ")
+(set 'notmuch-show-filename-regexp "filename:\\(.*\\)$")
(set 'notmuch-show-tags-regexp "(\\([^)]*\\))$")
; XXX: This should be a generic function in emacs somewhere, not here
(re-search-forward notmuch-show-tags-regexp)
(split-string (buffer-substring (match-beginning 1) (match-end 1)))))
-(defun notmuch-show-add-tag (tag)
+(defun notmuch-show-add-tag (&rest toadd)
+ "Add a tag to the current message."
(interactive "sTag to add: ")
- (notmuch-call-notmuch-process "tag" (concat "+" tag) (concat "id:" (notmuch-show-get-message-id)))
- (notmuch-show-set-tags (delete-dups (sort (cons tag (notmuch-show-get-tags)) 'string<))))
-
-(defun notmuch-show-remove-tag (tag)
+ (apply 'notmuch-call-notmuch-process
+ (append (cons "tag"
+ (mapcar (lambda (s) (concat "+" s)) toadd))
+ (cons (notmuch-show-get-message-id) nil)))
+ (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
+
+(defun notmuch-show-remove-tag (&rest toremove)
+ "Remove a tag from the current message."
(interactive "sTag to remove: ")
(let ((tags (notmuch-show-get-tags)))
- (if (member tag tags)
+ (if (intersection tags toremove :test 'string=)
(progn
- (notmuch-call-notmuch-process "tag" (concat "-" tag) (concat "id:" (notmuch-show-get-message-id)))
- (notmuch-show-set-tags (delete tag tags))))))
+ (apply 'notmuch-call-notmuch-process
+ (append (cons "tag"
+ (mapcar (lambda (s) (concat "-" s)) toremove))
+ (cons (notmuch-show-get-message-id) nil)))
+ (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
-(defun notmuch-show-archive-thread ()
- "Archive each message in thread, and show next thread from search.
-
-Archive each message currrently shown by removing the \"inbox\"
-tag from each. Then kill this buffer and show the next thread
-from the search from which this thread was originally shown.
-
-Note: This command is safe from any race condition of new messages
-being delivered to the same thread. It does not archive the
-entire thread, but only the messages shown in the current
-buffer."
- (interactive)
+(defun notmuch-show-archive-thread-maybe-mark-read (markread)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
- (notmuch-show-remove-tag "inbox")
+ (if markread
+ (notmuch-show-remove-tag "unread" "inbox")
+ (notmuch-show-remove-tag "inbox"))
(if (not (eobp))
(forward-char))
(if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
(if parent-buffer
(progn
(switch-to-buffer parent-buffer)
+ (forward-line)
(notmuch-search-show-thread)))))
+(defun notmuch-show-mark-read-then-archive-thread ()
+ "Remove \"unread\" tag from each message, then archive and show next thread.
+
+Archive each message currently shown by removing the \"unread\"
+and \"inbox\" tag from each. Then kill this buffer and show the
+next thread from the search from which this thread was originally
+shown.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+ (interactive)
+ (notmuch-show-archive-thread-maybe-mark-read t))
+
+(defun notmuch-show-archive-thread ()
+ "Archive each message in thread, and show next thread from search.
+
+Archive each message currently shown by removing the \"inbox\"
+tag from each. Then kill this buffer and show the next thread
+from the search from which this thread was originally shown.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+ (interactive)
+ (notmuch-show-archive-thread-maybe-mark-read nil))
+
(defun notmuch-show-view-raw-message ()
"View the raw email of the current message."
(interactive)
(view-file (notmuch-show-get-filename)))
+(defun notmuch-show-view-all-mime-parts ()
+ "Use external viewers (according to mailcap) to view all MIME-encoded parts."
+ (interactive)
+ (save-excursion
+ (let ((filename (notmuch-show-get-filename)))
+ (switch-to-buffer (generate-new-buffer (concat "*notmuch-mime-"
+ filename
+ "*")))
+ (insert-file-contents filename nil nil nil t)
+ (mm-display-parts (mm-dissect-buffer))
+ (kill-this-buffer))))
+
+(defun notmuch-reply (query-string)
+ (switch-to-buffer (generate-new-buffer "notmuch-draft"))
+ (call-process "notmuch" nil t nil "reply" query-string)
+ (goto-char (point-min))
+ (if (re-search-forward "^$" nil t)
+ (progn
+ (insert "--text follows this line--")
+ (forward-line)))
+ (message-mode))
+
+(defun notmuch-show-reply ()
+ "Begin composing a reply to the current message in a new buffer."
+ (interactive)
+ (let ((message-id (notmuch-show-get-message-id)))
+ (notmuch-reply message-id)))
+
+(defun notmuch-show-pipe-message (command)
+ "Pipe the contents of the current message to the given command.
+
+The given command will be executed with the raw contents of the
+current email message as stdin. Anything printed by the command
+to stdout or stderr will appear in the *Messages* buffer."
+ (interactive "sPipe message to command: ")
+ (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*" (split-string (concat command " < " (notmuch-show-get-filename)))))
+
(defun notmuch-show-move-to-current-message-summary-line ()
"Move to the beginning of the one-line summary of the current message.
(notmuch-show-next-message)))
(defun notmuch-show-next-open-message ()
- "Advance to the the next message which is not hidden.
+ "Advance to the next message which is not hidden.
If read messages are currently hidden, advance to the next unread
message. Otherwise, advance to the next message."
(unread (notmuch-show-message-unread-p)))
(if (> next (window-end))
(scroll-up nil)
- (if unread
- (notmuch-show-mark-read-then-next-open-message)
- (if (notmuch-show-last-message-p)
- (notmuch-show-archive-thread)
- (notmuch-show-next-open-message))))))
+ (let ((last (notmuch-show-last-message-p)))
+ (notmuch-show-mark-read-then-next-open-message)
+ (if last
+ (notmuch-show-archive-thread))))))
-(defun notmuch-show-markup-citations-region (beg end)
+(defun notmuch-show-markup-citations-region (beg end depth)
(goto-char beg)
(beginning-of-line)
(while (< (point) end)
- (let ((beg-sub (point)))
- (if (looking-at ">")
+ (let ((beg-sub (point-marker))
+ (indent (make-string depth ? ))
+ (citation "[[:space:]]*>"))
+ (if (looking-at citation)
(progn
- (while (looking-at ">")
+ (while (looking-at citation)
(forward-line))
(let ((overlay (make-overlay beg-sub (point))))
(overlay-put overlay 'invisible 'notmuch-show-citation)
(overlay-put overlay 'before-string
- (concat "[" (number-to-string (count-lines beg-sub (point)))
- " quoted lines.]\n")))))
- (if (looking-at "--[ ]?$")
- (let ((sig-lines (count-lines beg-sub end)))
+ (concat indent
+ "[" (number-to-string (count-lines beg-sub (point)))
+ "-line citation. Press 'c' to show.]\n")))))
+ (move-to-column depth)
+ (if (looking-at notmuch-show-signature-regexp)
+ (let ((sig-lines (- (count-lines beg-sub end) 1)))
(if (<= sig-lines notmuch-show-signature-lines-max)
(progn
- (overlay-put (make-overlay beg-sub (+ beg-sub 1))
- 'before-string
- (concat "[" (number-to-string sig-lines)
- "-line signature.]"))
- (overlay-put (make-overlay (+ beg-sub 2) end)
+ (overlay-put (make-overlay beg-sub end)
'invisible 'notmuch-show-signature)
+ (overlay-put (make-overlay beg (- beg-sub 1))
+ 'after-string
+ (concat "\n" indent
+ "[" (number-to-string sig-lines)
+ "-line signature. Press 's' to show.]"))
(goto-char end)))))
(forward-line))))
-(defun notmuch-show-markup-body ()
+(defun notmuch-show-markup-part (beg end depth)
+ (if (re-search-forward notmuch-show-part-begin-regexp nil t)
+ (progn
+ (forward-line)
+ (let ((beg (point-marker)))
+ (re-search-forward notmuch-show-part-end-regexp)
+ (let ((end (copy-marker (match-beginning 0))))
+ (goto-char end)
+ (if (not (bolp))
+ (insert "\n"))
+ (indent-rigidly beg end depth)
+ (notmuch-show-markup-citations-region beg end depth)
+ ; Advance to the next part (if any) (so the outer loop can
+ ; determine whether we've left the current message.
+ (if (re-search-forward notmuch-show-part-begin-regexp nil t)
+ (beginning-of-line)))))
+ (goto-char end)))
+
+(defun notmuch-show-markup-parts-region (beg end depth)
+ (save-excursion
+ (goto-char beg)
+ (while (< (point) end)
+ (notmuch-show-markup-part beg end depth))))
+
+(defun notmuch-show-markup-body (depth)
(re-search-forward notmuch-show-body-begin-regexp)
- (next-line 1)
- (beginning-of-line)
- (let ((beg (point)))
+ (forward-line)
+ (let ((beg (point-marker)))
(re-search-forward notmuch-show-body-end-regexp)
- (let ((end (match-beginning 0)))
- (notmuch-show-markup-citations-region beg end)
+ (let ((end (copy-marker (match-beginning 0))))
+ (notmuch-show-markup-parts-region beg end depth)
(if (not (notmuch-show-message-unread-p))
(overlay-put (make-overlay beg end)
- 'invisible 'notmuch-show-body-read)))))
+ 'invisible 'notmuch-show-body-read))
+ (set-marker beg nil)
+ (set-marker end nil)
+ )))
-(defun notmuch-show-markup-header ()
+(defun notmuch-show-markup-header (depth)
(re-search-forward notmuch-show-header-begin-regexp)
- (next-line 2)
- (beginning-of-line)
- (let ((beg (point)))
- (re-search-forward notmuch-show-header-end-regexp)
- (overlay-put (make-overlay beg (match-beginning 0))
- 'invisible 'notmuch-show-header)))
+ (forward-line)
+ (let ((beg (point-marker)))
+ (end-of-line)
+ ; Inverse video for subject
+ (overlay-put (make-overlay beg (point)) 'face '((cons :inverse-video t)))
+ (forward-line 2)
+ (let ((beg-hidden (point-marker)))
+ (re-search-forward notmuch-show-header-end-regexp)
+ (beginning-of-line)
+ (let ((end (point-marker)))
+ (indent-rigidly beg end depth)
+ (overlay-put (make-overlay beg-hidden end)
+ 'invisible 'notmuch-show-header)
+ (set-marker beg nil)
+ (set-marker beg-hidden nil)
+ (set-marker end nil)
+ ))))
(defun notmuch-show-markup-message ()
(if (re-search-forward notmuch-show-message-begin-regexp nil t)
(progn
- (notmuch-show-markup-header)
- (notmuch-show-markup-body))
+ (re-search-forward notmuch-show-depth-regexp)
+ (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1)))))
+ (notmuch-show-markup-header depth)
+ (notmuch-show-markup-body depth)))
(goto-char (point-max))))
(defun notmuch-show-hide-markers ()
mode-name "notmuch-show")
(setq buffer-read-only t))
+;;;###autoload
+
+(defgroup notmuch nil
+ "Notmuch mail reader for Emacs."
+ :group 'mail)
+
+(defcustom notmuch-show-hook nil
+ "List of functions to call when notmuch displays a message."
+ :type 'hook
+ :options '(goto-address)
+ :group 'notmuch)
+
+(defcustom notmuch-search-hook nil
+ "List of functions to call when notmuch displays the search results."
+ :type 'hook
+ :options '(hl-line-mode)
+ :group 'notmuch)
+
+; Make show mode a bit prettier, highlighting URLs and using word wrap
+
+(defun notmuch-show-pretty-hook ()
+ (goto-address-mode 1)
+ (visual-line-mode))
+
+(add-hook 'notmuch-show-hook 'notmuch-show-pretty-hook)
+(add-hook 'notmuch-search-hook
+ (lambda()
+ (hl-line-mode 1) ))
+
(defun notmuch-show (thread-id &optional parent-buffer)
"Run \"notmuch show\" with the given thread ID and display results.
(call-process "notmuch" nil t nil "show" thread-id)
(notmuch-show-markup-messages)
)
+ (run-hooks 'notmuch-show-hook)
; Move straight to the first unread message
(if (not (notmuch-show-message-unread-p))
(progn
(notmuch-show-toggle-body-read-visible)))))
)))
+(defvar notmuch-search-authors-width 40
+ "Number of columns to use to display authors in a notmuch-search buffer.")
+
(defvar notmuch-search-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "a" 'notmuch-search-archive-thread)
(define-key map "b" 'notmuch-search-scroll-down)
(define-key map "f" 'notmuch-search-filter)
+ (define-key map "m" 'message-mail)
(define-key map "n" 'next-line)
+ (define-key map "o" 'notmuch-search-toggle-order)
(define-key map "p" 'previous-line)
(define-key map "q" 'kill-this-buffer)
+ (define-key map "r" 'notmuch-search-reply-to-thread)
(define-key map "s" 'notmuch-search)
(define-key map "t" 'notmuch-search-filter-by-tag)
(define-key map "x" 'kill-this-buffer)
(define-key map "\M->" 'notmuch-search-goto-last-thread)
(define-key map " " 'notmuch-search-scroll-up)
(define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
+ (define-key map "?" 'describe-mode)
map)
"Keymap for \"notmuch search\" buffers.")
(fset 'notmuch-search-mode-map notmuch-search-mode-map)
(interactive)
(kill-all-local-variables)
(make-local-variable 'notmuch-search-query-string)
+ (make-local-variable 'notmuch-search-oldest-first)
(set (make-local-variable 'scroll-preserve-screen-position) t)
(add-to-invisibility-spec 'notmuch-search)
(use-local-map notmuch-search-mode-map)
+ (setq truncate-lines t)
(setq major-mode 'notmuch-search-mode
mode-name "notmuch-search")
(setq buffer-read-only t))
(save-excursion
(beginning-of-line)
(let ((beg (point)))
- (re-search-forward "[a-fA-F0-9]*")
+ (re-search-forward "thread:[a-fA-F0-9]*" nil t)
(filter-buffer-substring beg (point)))))
(defun notmuch-search-markup-this-thread-id ()
(beginning-of-line)
(let ((beg (point)))
- (re-search-forward "[a-fA-F0-9]*")
- (forward-char)
- (overlay-put (make-overlay beg (point)) 'invisible 'notmuch-search)))
+ (if (re-search-forward "thread:[a-fA-F0-9]*" nil t)
+ (progn
+ (forward-char)
+ (overlay-put (make-overlay beg (point)) 'invisible 'notmuch-search)
+ (re-search-forward ".*\\[[0-9]*/[0-9]*\\] \\([^;]*\\)\\(;\\)")
+ (let* ((authors (buffer-substring (match-beginning 1) (match-end 1)))
+ (authors-length (length authors)))
+ ;; Drop the semi-colon
+ (replace-match "" t nil nil 2)
+ (if (<= authors-length notmuch-search-authors-width)
+ (replace-match (concat authors (make-string
+ (- notmuch-search-authors-width
+ authors-length) ? )) t t nil 1)
+ (replace-match (concat (substring authors 0 (- notmuch-search-authors-width 3)) "...") t t nil 1)))))))
(defun notmuch-search-markup-thread-ids ()
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(notmuch-search-markup-this-thread-id)
- (next-line))))
-
-(defun notmuch-search-hide-thread-ids ()
- (interactive)
- (add-to-invisibility-spec 'notmuch-search)
- (force-window-update)
- (redisplay t))
-
-(defun notmuch-search-show-thread-ids ()
- (interactive)
- (remove-from-invisibility-spec 'notmuch-search)
- (force-window-update)
- (redisplay t))
+ (forward-line))))
(defun notmuch-search-show-thread ()
(interactive)
(let ((thread-id (notmuch-search-find-thread-id)))
- (forward-line)
(if (> (length thread-id) 0)
(notmuch-show thread-id (current-buffer))
(error "End of search results"))))
+(defun notmuch-search-reply-to-thread ()
+ "Begin composing a reply to the entire current thread in a new buffer."
+ (interactive)
+ (let ((message-id (notmuch-search-find-thread-id)))
+ (notmuch-reply message-id)))
+
(defun notmuch-call-notmuch-process (&rest args)
+ "Synchronously invoke \"notmuch\" with the given list of arguments.
+
+Output from the process will be presented to the user as an error
+and will also appear in a buffer named \"*Notmuch errors*\"."
(let ((error-buffer (get-buffer-create "*Notmuch errors*")))
(with-current-buffer error-buffer
(erase-buffer))
(defun notmuch-search-add-tag (tag)
(interactive "sTag to add: ")
- (notmuch-call-notmuch-process "tag" (concat "+" tag) (concat "thread:" (notmuch-search-find-thread-id)))
+ (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id))
(notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
(defun notmuch-search-remove-tag (tag)
(interactive "sTag to remove: ")
- (notmuch-call-notmuch-process "tag" (concat "-" tag) (concat "thread:" (notmuch-search-find-thread-id)))
+ (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id))
(notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
(defun notmuch-search-archive-thread ()
(notmuch-search-remove-tag "inbox")
(forward-line))
-(defun notmuch-search (query)
+(defun notmuch-search (query &optional oldest-first)
"Run \"notmuch search\" with the given query string and display results."
(interactive "sNotmuch search: ")
(let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
(switch-to-buffer buffer)
(notmuch-search-mode)
(set 'notmuch-search-query-string query)
+ (set 'notmuch-search-oldest-first oldest-first)
(let ((proc (get-buffer-process (current-buffer)))
(inhibit-read-only t))
(if proc
(erase-buffer)
(goto-char (point-min))
(save-excursion
- (call-process "notmuch" nil t nil "search" query)
+ (if oldest-first
+ (call-process "notmuch" nil t nil "search" "--sort=oldest-first" query)
+ (call-process "notmuch" nil t nil "search" "--sort=newest-first" query))
(notmuch-search-markup-thread-ids)
- ))))
+ ))
+ (run-hooks 'notmuch-search-hook)))
(defun notmuch-search-refresh-view ()
"Refresh the current view.
same relative position within the new buffer."
(interactive)
(let ((here (point))
+ (oldest-first notmuch-search-oldest-first)
(thread (notmuch-search-find-thread-id))
(query notmuch-search-query-string))
(kill-this-buffer)
- (notmuch-search query)
+ (notmuch-search query oldest-first)
(goto-char (point-min))
(if (re-search-forward (concat "^" thread) nil t)
(beginning-of-line)
(goto-char here))))
+(defun notmuch-search-toggle-order ()
+ "Toggle the current search order.
+
+By default, the \"inbox\" view created by `notmuch' is displayed
+in chronological order (oldest thread at the beginning of the
+buffer), while any global searches created by `notmuch-search'
+are displayed in reverse-chronological order (newest thread at
+the beginning of the buffer).
+
+This command toggles the sort order for the current search.
+
+Note that any filtered searches created by
+`notmuch-search-filter' retain the search order of the parent
+search."
+ (interactive)
+ (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
+ (notmuch-search-refresh-view))
+
(defun notmuch-search-filter (query)
"Filter the current search results based on an additional query string.
Runs a new search matching only messages that match both the
current search results AND the additional query string provided."
(interactive "sFilter search: ")
- (notmuch-search (concat notmuch-search-query-string " and " query)))
+ (notmuch-search (concat notmuch-search-query-string " and " query) notmuch-search-oldest-first))
(defun notmuch-search-filter-by-tag (tag)
"Filter the current search results based on a single tag.
Runs a new search matching only messages that match both the
current search results AND that are tagged with the given tag."
(interactive "sFilter by tag: ")
- (notmuch-search (concat notmuch-search-query-string " and tag:" tag)))
+ (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
(defun notmuch ()
"Run notmuch to display all mail with tag of 'inbox'"
(interactive)
- (notmuch-search "tag:inbox"))
+ (notmuch-search "tag:inbox" t))
+
+(setq mail-user-agent 'message-user-agent)
(provide 'notmuch)