aboutsummaryrefslogtreecommitdiffhomepage
path: root/emacs
diff options
context:
space:
mode:
Diffstat (limited to 'emacs')
-rw-r--r--emacs/Makefile.local3
-rw-r--r--emacs/notmuch-address.el9
-rw-r--r--emacs/notmuch-crypto.el22
-rw-r--r--emacs/notmuch-hello.el151
-rw-r--r--emacs/notmuch-lib.el66
-rw-r--r--emacs/notmuch-maildir-fcc.el6
-rw-r--r--emacs/notmuch-message.el2
-rw-r--r--emacs/notmuch-mua.el33
-rw-r--r--emacs/notmuch-print.el92
-rw-r--r--emacs/notmuch-show.el482
-rw-r--r--emacs/notmuch-wash.el106
-rw-r--r--emacs/notmuch.el351
12 files changed, 908 insertions, 415 deletions
diff --git a/emacs/Makefile.local b/emacs/Makefile.local
index 0c58b824..4fee0e89 100644
--- a/emacs/Makefile.local
+++ b/emacs/Makefile.local
@@ -13,7 +13,8 @@ emacs_sources := \
$(dir)/notmuch-maildir-fcc.el \
$(dir)/notmuch-message.el \
$(dir)/notmuch-crypto.el \
- $(dir)/coolj.el
+ $(dir)/coolj.el \
+ $(dir)/notmuch-print.el
emacs_images := \
$(srcdir)/$(dir)/notmuch-logo.png
diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el
index 8eba7a0b..2bf762ba 100644
--- a/emacs/notmuch-address.el
+++ b/emacs/notmuch-address.el
@@ -28,7 +28,8 @@
single argument and output a list of possible matches, one per
line."
:type 'string
- :group 'notmuch)
+ :group 'notmuch-send
+ :group 'notmuch-external)
(defvar notmuch-address-message-alist-member
'("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):"
@@ -37,9 +38,9 @@ line."
(defvar notmuch-address-history nil)
(defun notmuch-address-message-insinuate ()
- (if (not (memq notmuch-address-message-alist-member message-completion-alist))
- (setq message-completion-alist
- (push notmuch-address-message-alist-member message-completion-alist))))
+ (unless (memq notmuch-address-message-alist-member message-completion-alist)
+ (setq message-completion-alist
+ (push notmuch-address-message-alist-member message-completion-alist))))
(defun notmuch-address-options (original)
(process-lines notmuch-address-command original))
diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el
index ac300987..80ac350e 100644
--- a/emacs/notmuch-crypto.el
+++ b/emacs/notmuch-crypto.el
@@ -34,38 +34,44 @@ The effect of setting this variable can be seen temporarily by
providing a prefix when viewing a signed or encrypted message, or
by providing a prefix when reloading the message in notmuch-show
mode."
- :group 'notmuch
- :type 'boolean)
+ :type 'boolean
+ :group 'notmuch-crypto)
(defface notmuch-crypto-part-header
'((t (:foreground "blue")))
"Face used for crypto parts headers."
- :group 'notmuch)
+ :group 'notmuch-crypto
+ :group 'notmuch-faces)
(defface notmuch-crypto-signature-good
'((t (:background "green" :foreground "black")))
"Face used for good signatures."
- :group 'notmuch)
+ :group 'notmuch-crypto
+ :group 'notmuch-faces)
(defface notmuch-crypto-signature-good-key
'((t (:background "orange" :foreground "black")))
"Face used for good signatures."
- :group 'notmuch)
+ :group 'notmuch-crypto
+ :group 'notmuch-faces)
(defface notmuch-crypto-signature-bad
'((t (:background "red" :foreground "black")))
"Face used for bad signatures."
- :group 'notmuch)
+ :group 'notmuch-crypto
+ :group 'notmuch-faces)
(defface notmuch-crypto-signature-unknown
'((t (:background "red" :foreground "black")))
"Face used for signatures of unknown status."
- :group 'notmuch)
+ :group 'notmuch-crypto
+ :group 'notmuch-faces)
(defface notmuch-crypto-decryption
'((t (:background "purple" :foreground "black")))
"Face used for encryption/decryption status messages."
- :group 'notmuch)
+ :group 'notmuch-crypto
+ :group 'notmuch-faces)
(define-button-type 'notmuch-crypto-status-button-type
'action (lambda (button) (message (button-get button 'help-echo)))
diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el
index 333d4c1e..d17a30f9 100644
--- a/emacs/notmuch-hello.el
+++ b/emacs/notmuch-hello.el
@@ -29,18 +29,15 @@
(declare-function notmuch-search "notmuch" (query &optional oldest-first target-thread target-line continuation))
(declare-function notmuch-poll "notmuch" ())
-(defvar notmuch-hello-search-bar-marker nil
- "The position of the search bar within the notmuch-hello buffer.")
-
-(defcustom notmuch-recent-searches-max 10
- "The number of recent searches to store and display."
+(defcustom notmuch-hello-recent-searches-max 10
+ "The number of recent searches to display."
:type 'integer
- :group 'notmuch)
+ :group 'notmuch-hello)
(defcustom notmuch-show-empty-saved-searches nil
"Should saved searches with no messages be listed?"
:type 'boolean
- :group 'notmuch)
+ :group 'notmuch-hello)
(defun notmuch-sort-saved-searches (alist)
"Generate an alphabetically sorted saved searches alist."
@@ -60,7 +57,7 @@ alist to be used."
(const :tag "Sort alphabetically" notmuch-sort-saved-searches)
(function :tag "Custom sort function"
:value notmuch-sort-saved-searches))
- :group 'notmuch)
+ :group 'notmuch-hello)
(defvar notmuch-hello-indent 4
"How much to indent non-headers.")
@@ -68,12 +65,12 @@ alist to be used."
(defcustom notmuch-show-logo t
"Should the notmuch logo be shown?"
:type 'boolean
- :group 'notmuch)
+ :group 'notmuch-hello)
(defcustom notmuch-show-all-tags-list nil
"Should all tags be shown in the notmuch-hello view?"
:type 'boolean
- :group 'notmuch)
+ :group 'notmuch-hello)
(defcustom notmuch-hello-tag-list-make-query nil
"Function or string to generate queries for the all tags list.
@@ -89,12 +86,12 @@ should return a filter for that tag, or nil to hide the tag."
(string :tag "Custom filter"
:value "tag:unread")
(function :tag "Custom filter function"))
- :group 'notmuch)
+ :group 'notmuch-hello)
(defcustom notmuch-hello-hide-tags nil
"List of tags to be hidden in the \"all tags\"-section."
:type '(repeat string)
- :group 'notmuch)
+ :group 'notmuch-hello)
(defface notmuch-hello-logo-background
'((((class color)
@@ -104,7 +101,8 @@ should return a filter for that tag, or nil to hide the tag."
(background light))
(:background "white")))
"Background colour for the notmuch logo."
- :group 'notmuch)
+ :group 'notmuch-hello
+ :group 'notmuch-faces)
(defcustom notmuch-column-control t
"Controls the number of columns for saved searches/tags in notmuch view.
@@ -126,11 +124,11 @@ So:
30.
- if you don't want to worry about all of this nonsense, leave
this set to `t'."
- :group 'notmuch
:type '(choice
(const :tag "Automatically calculated" t)
(integer :tag "Number of characters")
- (float :tag "Fraction of window")))
+ (float :tag "Fraction of window"))
+ :group 'notmuch-hello)
(defcustom notmuch-hello-thousands-separator " "
"The string used as a thousands separator.
@@ -138,32 +136,24 @@ So:
Typically \",\" in the US and UK and \".\" or \" \" in Europe.
The latter is recommended in the SI/ISO 31-0 standard and by the
International Bureau of Weights and Measures."
- :group 'notmuch
- :type 'string)
+ :type 'string
+ :group 'notmuch-hello)
(defcustom notmuch-hello-mode-hook nil
"Functions called after entering `notmuch-hello-mode'."
- :group 'notmuch
- :type 'hook)
+ :type 'hook
+ :group 'notmuch-hello
+ :group 'notmuch-hooks)
(defcustom notmuch-hello-refresh-hook nil
"Functions called after updating a `notmuch-hello' buffer."
:type 'hook
- :group 'notmuch)
+ :group 'notmuch-hello
+ :group 'notmuch-hooks)
(defvar notmuch-hello-url "http://notmuchmail.org"
"The `notmuch' web site.")
-(defvar notmuch-hello-recent-searches nil)
-
-(defun notmuch-hello-remember-search (search)
- (setq notmuch-hello-recent-searches
- (delete search notmuch-hello-recent-searches))
- (push search notmuch-hello-recent-searches)
- (if (> (length notmuch-hello-recent-searches)
- notmuch-recent-searches-max)
- (setq notmuch-hello-recent-searches (butlast notmuch-hello-recent-searches))))
-
(defun notmuch-hello-nice-number (n)
(let (result)
(while (> n 0)
@@ -182,10 +172,14 @@ International Bureau of Weights and Measures."
(match-string 1 search)
search))
-(defun notmuch-hello-search (search)
- (let ((search (notmuch-hello-trim search)))
- (notmuch-hello-remember-search search)
- (notmuch-search search notmuch-search-oldest-first nil nil #'notmuch-hello-search-continuation)))
+(defun notmuch-hello-search (&optional search)
+ (interactive)
+ (unless (null search)
+ (setq search (notmuch-hello-trim search))
+ (let ((history-delete-duplicates t))
+ (add-to-history 'notmuch-search-history search)))
+ (notmuch-search search notmuch-search-oldest-first nil nil
+ #'notmuch-hello-search-continuation))
(defun notmuch-hello-add-saved-search (widget)
(interactive)
@@ -299,15 +293,17 @@ should be. Returns a cons cell `(tags-per-line width)'."
:notify #'notmuch-hello-widget-search
:notmuch-search-terms query
formatted-name)
- ;; Insert enough space to consume the rest of the
- ;; column. Because the button for the name is `(1+
- ;; (length name))' long (due to the trailing space) we
- ;; can just insert `(- widest (length name))' spaces -
- ;; the column separator is included in the button if
- ;; `(equal widest (length name)'.
- (widget-insert (make-string (max 1
- (- widest (length name)))
- ? ))))
+ (unless (eq (% count tags-per-line) (1- tags-per-line))
+ ;; If this is not the last tag on the line, insert
+ ;; enough space to consume the rest of the column.
+ ;; Because the button for the name is `(1+ (length
+ ;; name))' long (due to the trailing space) we can
+ ;; just insert `(- widest (length name))' spaces - the
+ ;; column separator is included in the button if
+ ;; `(equal widest (length name)'.
+ (widget-insert (make-string (max 1
+ (- widest (length name)))
+ ? )))))
(setq count (1+ count))
(if (eq (% count tags-per-line) 0)
(widget-insert "\n")))
@@ -315,15 +311,10 @@ should be. Returns a cons cell `(tags-per-line width)'."
;; If the last line was not full (and hence did not include a
;; carriage return), insert one now.
- (if (not (eq (% count tags-per-line) 0))
- (widget-insert "\n"))
+ (unless (eq (% count tags-per-line) 0)
+ (widget-insert "\n"))
found-target-pos))
-(defun notmuch-hello-goto-search ()
- "Put point inside the `search' widget."
- (interactive)
- (goto-char notmuch-hello-search-bar-marker))
-
(defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
(defun notmuch-hello-search-continuation()
@@ -353,7 +344,7 @@ should be. Returns a cons cell `(tags-per-line width)'."
(define-key map "G" 'notmuch-hello-poll-and-update)
(define-key map (kbd "<C-tab>") 'widget-backward)
(define-key map "m" 'notmuch-mua-new-mail)
- (define-key map "s" 'notmuch-hello-goto-search)
+ (define-key map "s" 'notmuch-hello-search)
map)
"Keymap for \"notmuch hello\" buffers.")
(fset 'notmuch-hello-mode-map notmuch-hello-mode-map)
@@ -397,9 +388,9 @@ Complete list of currently available key bindings:
"Run notmuch and display saved searches, known tags, etc."
(interactive)
- ; Jump through a hoop to get this value from the deprecated variable
- ; name (`notmuch-folders') or from the default value.
- (if (not notmuch-saved-searches)
+ ;; Jump through a hoop to get this value from the deprecated variable
+ ;; name (`notmuch-folders') or from the default value.
+ (unless notmuch-saved-searches
(setq notmuch-saved-searches (notmuch-saved-searches)))
(if no-display
@@ -466,7 +457,8 @@ Complete list of currently available key bindings:
(widget-insert " messages.\n"))
(let ((found-target-pos nil)
- (final-target-pos nil))
+ (final-target-pos nil)
+ (default-pos))
(let* ((saved-alist
;; Filter out empty saved searches if required.
(if notmuch-show-empty-saved-searches
@@ -498,7 +490,7 @@ Complete list of currently available key bindings:
(indent-rigidly start (point) notmuch-hello-indent)))
(widget-insert "\nSearch: ")
- (setq notmuch-hello-search-bar-marker (point-marker))
+ (setq default-pos (point-marker))
(widget-create 'editable-field
;; Leave some space at the start and end of the
;; search boxes.
@@ -506,24 +498,27 @@ Complete list of currently available key bindings:
(length "Search: ")))
:action (lambda (widget &rest ignore)
(notmuch-hello-search (widget-value widget))))
- ;; add an invisible space to make `widget-end-of-line' ignore
- ;; trailine spaces in the search widget field
- (widget-insert " ")
+ ;; Add an invisible dot to make `widget-end-of-line' ignore
+ ;; trailing spaces in the search widget field. A dot is used
+ ;; instead of a space to make `show-trailing-whitespace'
+ ;; happy, i.e. avoid it marking the whole line as trailing
+ ;; spaces.
+ (widget-insert ".")
(put-text-property (1- (point)) (point) 'invisible t)
(widget-insert "\n")
- (when notmuch-hello-recent-searches
+ (when notmuch-search-history
(widget-insert "\nRecent searches: ")
(widget-create 'push-button
:notify (lambda (&rest ignore)
- (setq notmuch-hello-recent-searches nil)
+ (setq notmuch-search-history nil)
(notmuch-hello-update))
"clear")
(widget-insert "\n\n")
- (let ((start (point))
- (nth 0))
- (mapc (lambda (search)
- (let ((widget-symbol (intern (format "notmuch-hello-search-%d" nth))))
+ (let ((start (point)))
+ (loop for i from 1 to notmuch-hello-recent-searches-max
+ for search in notmuch-search-history do
+ (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
(set widget-symbol
(widget-create 'editable-field
;; Don't let the search boxes be
@@ -550,9 +545,7 @@ Complete list of currently available key bindings:
(notmuch-hello-add-saved-search widget))
:notmuch-saved-search-widget widget-symbol
"save"))
- (widget-insert "\n")
- (setq nth (1+ nth)))
- notmuch-hello-recent-searches)
+ (widget-insert "\n"))
(indent-rigidly start (point) notmuch-hello-indent)))
(when alltags-alist
@@ -565,29 +558,29 @@ Complete list of currently available key bindings:
(widget-insert "\n\n")
(let ((start (point)))
(setq found-target-pos (notmuch-hello-insert-tags alltags-alist widest target))
- (if (not final-target-pos)
- (setq final-target-pos found-target-pos))
+ (unless final-target-pos
+ (setq final-target-pos found-target-pos))
(indent-rigidly start (point) notmuch-hello-indent)))
(widget-insert "\n")
- (if (not notmuch-show-all-tags-list)
- (widget-create 'push-button
- :notify (lambda (widget &rest ignore)
- (setq notmuch-show-all-tags-list t)
- (notmuch-hello-update))
- "Show all tags")))
+ (unless notmuch-show-all-tags-list
+ (widget-create 'push-button
+ :notify (lambda (widget &rest ignore)
+ (setq notmuch-show-all-tags-list t)
+ (notmuch-hello-update))
+ "Show all tags")))
(let ((start (point)))
(widget-insert "\n\n")
(widget-insert "Type a search query and hit RET to view matching threads.\n")
- (when notmuch-hello-recent-searches
+ (when notmuch-search-history
(widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n")
(widget-insert "Save recent searches with the `save' button.\n"))
(when notmuch-saved-searches
(widget-insert "Edit saved searches with the `edit' button.\n"))
(widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n")
- (widget-insert "`=' refreshes this screen. `s' jumps to the search box. `q' to quit.\n")
+ (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
(let ((fill-column (- (window-width) notmuch-hello-indent)))
(center-region start (point))))
@@ -599,7 +592,7 @@ Complete list of currently available key bindings:
(widget-forward 1)))
(unless (widget-at)
- (notmuch-hello-goto-search))))
+ (goto-char default-pos))))
(run-hooks 'notmuch-hello-refresh-hook))
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
index 0f856bf0..d315f765 100644
--- a/emacs/notmuch-lib.el
+++ b/emacs/notmuch-lib.el
@@ -28,17 +28,54 @@
"Notmuch mail reader for Emacs."
:group 'mail)
+(defgroup notmuch-hello nil
+ "Overview of saved searches, tags, etc."
+ :group 'notmuch)
+
+(defgroup notmuch-search nil
+ "Searching and sorting mail."
+ :group 'notmuch)
+
+(defgroup notmuch-show nil
+ "Showing messages and threads."
+ :group 'notmuch)
+
+(defgroup notmuch-send nil
+ "Sending messages from Notmuch."
+ :group 'notmuch)
+
+(custom-add-to-group 'notmuch-send 'message 'custom-group)
+
+(defgroup notmuch-crypto nil
+ "Processing and display of cryptographic MIME parts."
+ :group 'notmuch)
+
+(defgroup notmuch-hooks nil
+ "Running custom code on well-defined occasions."
+ :group 'notmuch)
+
+(defgroup notmuch-external nil
+ "Running external commands from within Notmuch."
+ :group 'notmuch)
+
+(defgroup notmuch-faces nil
+ "Graphical attributes for displaying text"
+ :group 'notmuch)
+
(defcustom notmuch-search-oldest-first t
"Show the oldest mail first when searching."
:type 'boolean
- :group 'notmuch)
+ :group 'notmuch-search)
;;
+(defvar notmuch-search-history nil
+ "Variable to store notmuch searches history.")
+
(defcustom notmuch-saved-searches nil
"A list of saved searches to display."
:type '(alist :key-type string :value-type string)
- :group 'notmuch)
+ :group 'notmuch-hello)
(defvar notmuch-folders nil
"Deprecated name for what is now known as `notmuch-saved-searches'.")
@@ -96,6 +133,15 @@ the user hasn't set this variable with the old or new value."
(interactive)
(kill-buffer (current-buffer)))
+(defun notmuch-prettify-subject (subject)
+ ;; This function is used by `notmuch-search-process-filter' which
+ ;; requires that we not disrupt its' matching state.
+ (save-match-data
+ (if (and subject
+ (string-match "^[ \t]*$" subject))
+ "[No Subject]"
+ subject)))
+
;;
(defun notmuch-common-do-stash (text)
@@ -114,14 +160,14 @@ the user hasn't set this variable with the old or new value."
(setq list (cdr list)))
(nreverse out)))
-; This lets us avoid compiling these replacement functions when emacs
-; is sufficiently new enough to supply them alone. We do the macro
-; treatment rather than just wrapping our defun calls in a when form
-; specifically so that the compiler never sees the code on new emacs,
-; (since the code is triggering warnings that we don't know how to get
-; rid of.
-;
-; A more clever macro here would accept a condition and a list of forms.
+;; This lets us avoid compiling these replacement functions when emacs
+;; is sufficiently new enough to supply them alone. We do the macro
+;; treatment rather than just wrapping our defun calls in a when form
+;; specifically so that the compiler never sees the code on new emacs,
+;; (since the code is triggering warnings that we don't know how to get
+;; rid of.
+;;
+;; A more clever macro here would accept a condition and a list of forms.
(defmacro compile-on-emacs-prior-to-23 (form)
"Conditionally evaluate form only on emacs < emacs-23."
(list 'when (< emacs-major-version 23)
diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el
index 6fbf82d2..dcfbc4b3 100644
--- a/emacs/notmuch-maildir-fcc.el
+++ b/emacs/notmuch-maildir-fcc.el
@@ -51,13 +51,13 @@ the database.path option in the notmuch configuration file).
You will be prompted to create the directory if it does not exist
yet when sending a mail."
- :require 'notmuch-fcc-initialization
- :group 'notmuch
:type '(choice
(const :tag "No FCC header" nil)
(string :tag "A single folder")
(repeat :tag "A folder based on the From header"
- (cons regexp (string :tag "Folder")))))
+ (cons regexp (string :tag "Folder"))))
+ :require 'notmuch-fcc-initialization
+ :group 'notmuch-send)
(defun notmuch-fcc-initialization ()
"If notmuch-fcc-directories is set,
diff --git a/emacs/notmuch-message.el b/emacs/notmuch-message.el
index 08e5b174..264a5b9b 100644
--- a/emacs/notmuch-message.el
+++ b/emacs/notmuch-message.el
@@ -31,7 +31,7 @@ For example, if you wanted to add a \"replied\" tag and remove
the \"inbox\" and \"todo\", you would set
(\"replied\" \"-inbox\" \"-todo\"\)"
:type 'list
- :group 'notmuch)
+ :group 'notmuch-send)
(defun notmuch-message-mark-replied ()
;; get the in-reply-to header and parse it for the message id.
diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el
index 3e93d7c8..c07b67ba 100644
--- a/emacs/notmuch-mua.el
+++ b/emacs/notmuch-mua.el
@@ -28,25 +28,26 @@
(defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook)
"Hook run before sending messages."
- :group 'notmuch
- :type 'hook)
+ :type 'hook
+ :group 'notmuch-send
+ :group 'notmuch-hooks)
(defcustom notmuch-mua-user-agent-function 'notmuch-mua-user-agent-full
"Function used to generate a `User-Agent:' string. If this is
`nil' then no `User-Agent:' will be generated."
- :group 'notmuch
:type '(choice (const :tag "No user agent string" nil)
(const :tag "Full" notmuch-mua-user-agent-full)
(const :tag "Notmuch" notmuch-mua-user-agent-notmuch)
(const :tag "Emacs" notmuch-mua-user-agent-emacs)
(function :tag "Custom user agent function"
- :value notmuch-mua-user-agent-full)))
+ :value notmuch-mua-user-agent-full))
+ :group 'notmuch-send)
(defcustom notmuch-mua-hidden-headers '("^User-Agent:")
"Headers that are added to the `message-mode' hidden headers
list."
- :group 'notmuch
- :type '(repeat string))
+ :type '(repeat string)
+ :group 'notmuch-send)
;;
@@ -71,12 +72,15 @@ list."
(push header message-hidden-headers)))
notmuch-mua-hidden-headers))
-(defun notmuch-mua-reply (query-string &optional sender)
+(defun notmuch-mua-reply (query-string &optional sender reply-all)
(let (headers
body
(args '("reply")))
(if notmuch-show-process-crypto
(setq args (append args '("--decrypt"))))
+ (if reply-all
+ (setq args (append args '("--reply-to=all")))
+ (setq args (append args '("--reply-to=sender"))))
(setq args (append args (list query-string)))
;; This make assumptions about the output of `notmuch reply', but
;; really only that the headers come first followed by a blank
@@ -108,7 +112,8 @@ list."
(if (re-search-backward message-signature-separator nil t)
(forward-line -1)
(goto-char (point-max)))
- (insert body))
+ (insert body)
+ (push-mark))
(set-buffer-modified-p nil)
(message-goto-body)
@@ -158,16 +163,16 @@ OTHER-ARGS are passed through to `message-mail'."
If this variable is left unset, then a list will be constructed from the
name and addresses configured in the notmuch configuration file."
- :group 'notmuch
- :type '(repeat string))
+ :type '(repeat string)
+ :group 'notmuch-send)
(defcustom notmuch-always-prompt-for-sender nil
"Always prompt for the From: address when composing or forwarding a message.
This is not taken into account when replying to a message, because in that case
the From: header is already filled in by notmuch."
- :group 'notmuch
- :type 'boolean)
+ :type 'boolean
+ :group 'notmuch-send)
(defvar notmuch-mua-sender-history nil)
@@ -222,13 +227,13 @@ the From: address first."
(notmuch-mua-forward-message))
(notmuch-mua-forward-message)))
-(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender)
+(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
"Invoke the notmuch reply window."
(interactive "P")
(let ((sender
(when prompt-for-sender
(notmuch-mua-prompt-for-sender))))
- (notmuch-mua-reply query-string sender)))
+ (notmuch-mua-reply query-string sender reply-all)))
(defun notmuch-mua-send-and-exit (&optional arg)
(interactive "P")
diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el
new file mode 100644
index 00000000..6653d977
--- /dev/null
+++ b/emacs/notmuch-print.el
@@ -0,0 +1,92 @@
+;; notmuch-print.el --- printing messages from notmuch.
+;;
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch 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.
+;;
+;; Notmuch 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 Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+
+(require 'notmuch-lib)
+
+(declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props))
+
+(defcustom notmuch-print-mechanism 'notmuch-print-lpr
+ "How should printing be done?"
+ :group 'notmuch
+ :type '(choice
+ (function :tag "Use lpr" notmuch-print-lpr)
+ (function :tag "Use ps-print" notmuch-print-ps-print)
+ (function :tag "Use ps-print then evince" notmuch-print-ps-print/evince)
+ (function :tag "Use muttprint" notmuch-print-muttprint)
+ (function :tag "Use muttprint then evince" notmuch-print-muttprint/evince)
+ (function :tag "Using a custom function")))
+
+;; Utility functions:
+
+(defun notmuch-print-run-evince (file)
+ "View FILE using 'evince'."
+ (start-process "evince" nil "evince" file))
+
+(defun notmuch-print-run-muttprint (&optional output)
+ "Pass the contents of the current buffer to 'muttprint'.
+
+Optional OUTPUT allows passing a list of flags to muttprint."
+ (apply #'call-process-region (point-min) (point-max)
+ ;; Reads from stdin.
+ "muttprint"
+ nil nil nil
+ ;; Show the tags.
+ "--printed-headers" "Date_To_From_CC_Newsgroups_*Subject*_/Tags/"
+ output))
+
+;; User-visible functions:
+
+(defun notmuch-print-lpr (msg)
+ "Print a message buffer using lpr."
+ (lpr-buffer))
+
+(defun notmuch-print-ps-print (msg)
+ "Print a message buffer using the ps-print package."
+ (let ((subject (notmuch-prettify-subject
+ (plist-get (notmuch-show-get-prop :headers msg) :Subject))))
+ (rename-buffer subject t)
+ (ps-print-buffer)))
+
+(defun notmuch-print-ps-print/evince (msg)
+ "Preview a message buffer using ps-print and evince."
+ (let ((ps-file (make-temp-file "notmuch"))
+ (subject (notmuch-prettify-subject
+ (plist-get (notmuch-show-get-prop :headers msg) :Subject))))
+ (rename-buffer subject t)
+ (ps-print-buffer ps-file)
+ (notmuch-print-run-evince ps-file)))
+
+(defun notmuch-print-muttprint (msg)
+ "Print a message using muttprint."
+ (notmuch-print-run-muttprint))
+
+(defun notmuch-print-muttprint/evince (msg)
+ "Preview a message buffer using muttprint and evince."
+ (let ((ps-file (make-temp-file "notmuch")))
+ (notmuch-print-run-muttprint (list "--printer" (concat "TO_FILE:" ps-file)))
+ (notmuch-print-run-evince ps-file)))
+
+(defun notmuch-print-message (msg)
+ "Print a message using the user-selected mechanism."
+ (set-buffer-modified-p nil)
+ (funcall notmuch-print-mechanism msg))
+
+(provide 'notmuch-print)
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
index 82d11c92..7469e2eb 100644
--- a/emacs/notmuch-show.el
+++ b/emacs/notmuch-show.el
@@ -34,10 +34,12 @@
(require 'notmuch-wash)
(require 'notmuch-mua)
(require 'notmuch-crypto)
+(require 'notmuch-print)
(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
(declare-function notmuch-fontify-headers "notmuch" nil)
(declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms))
+(declare-function notmuch-search-next-thread "notmuch" nil)
(declare-function notmuch-search-show-thread "notmuch" nil)
(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
@@ -47,8 +49,8 @@ For an open message, all of these headers will be made visible
according to `notmuch-message-headers-visible' or can be toggled
with `notmuch-show-toggle-headers'. For a closed message, only
the first header in the list will be visible."
- :group 'notmuch
- :type '(repeat string))
+ :type '(repeat string)
+ :group 'notmuch-show)
(defcustom notmuch-message-headers-visible t
"Should the headers be visible by default?
@@ -58,38 +60,44 @@ If this value is non-nil, then all of the headers defined in
of each message. Otherwise, these headers will be hidden and
`notmuch-show-toggle-headers' can be used to make the visible for
any given message."
- :group 'notmuch
- :type 'boolean)
+ :type 'boolean
+ :group 'notmuch-show)
(defcustom notmuch-show-relative-dates t
"Display relative dates in the message summary line."
- :group 'notmuch
- :type 'boolean)
+ :type 'boolean
+ :group 'notmuch-show)
(defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
"A list of functions called to decorate the headers listed in
`notmuch-message-headers'.")
-(defcustom notmuch-show-hook nil
+(defcustom notmuch-show-hook '(notmuch-show-turn-on-visual-line-mode)
"Functions called after populating a `notmuch-show' buffer."
- :group 'notmuch
- :type 'hook)
-
-(defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-excerpt-citations)
+ :type 'hook
+ :options '(notmuch-show-turn-on-visual-line-mode)
+ :group 'notmuch-show
+ :group 'notmuch-hooks)
+
+(defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-wrap-long-lines
+ notmuch-wash-tidy-citations
+ notmuch-wash-elide-blank-lines
+ notmuch-wash-excerpt-citations)
"Functions used to improve the display of text/plain parts."
- :group 'notmuch
:type 'hook
:options '(notmuch-wash-convert-inline-patch-to-part
notmuch-wash-wrap-long-lines
notmuch-wash-tidy-citations
notmuch-wash-elide-blank-lines
- notmuch-wash-excerpt-citations))
+ notmuch-wash-excerpt-citations)
+ :group 'notmuch-show
+ :group 'notmuch-hooks)
;; Mostly useful for debugging.
(defcustom notmuch-show-all-multipart/alternative-parts t
"Should all parts of multipart/alternative parts be shown?"
- :group 'notmuch
- :type 'boolean)
+ :type 'boolean
+ :group 'notmuch-show)
(defcustom notmuch-show-indent-messages-width 1
"Width of message indentation in threads.
@@ -98,14 +106,24 @@ Messages are shown indented according to their depth in a thread.
This variable determines the width of this indentation measured
in number of blanks. Defaults to `1', choose `0' to disable
indentation."
- :group 'notmuch
- :type 'integer)
+ :type 'integer
+ :group 'notmuch-show)
(defcustom notmuch-show-indent-multipart nil
"Should the sub-parts of a multipart/* part be indented?"
;; dme: Not sure which is a good default.
- :group 'notmuch
- :type 'boolean)
+ :type 'boolean
+ :group 'notmuch-show)
+
+(defcustom notmuch-show-part-button-default-action 'notmuch-show-save-part
+ "Default part header button action (on ENTER or mouse click)."
+ :group 'notmuch-show
+ :type '(choice (const :tag "Save part"
+ notmuch-show-save-part)
+ (const :tag "View part"
+ notmuch-show-view-part)
+ (const :tag "View interactively"
+ notmuch-show-interactively-view-part)))
(defmacro with-current-notmuch-show-message (&rest body)
"Evaluate body with current buffer set to the text of current message"
@@ -117,18 +135,22 @@ indentation."
,@body)
(kill-buffer buf)))))
+(defun notmuch-show-turn-on-visual-line-mode ()
+ "Enable Visual Line mode."
+ (visual-line-mode t))
+
(defun notmuch-show-view-all-mime-parts ()
"Use external viewers to view all attachments from the current message."
(interactive)
(with-current-notmuch-show-message
- ; We override the mm-inline-media-tests to indicate which message
- ; parts are already sufficiently handled by the original
- ; presentation of the message in notmuch-show mode. These parts
- ; will be inserted directly into the temporary buffer of
- ; with-current-notmuch-show-message and silently discarded.
- ;
- ; Any MIME part not explicitly mentioned here will be handled by an
- ; external viewer as configured in the various mailcap files.
+ ;; We override the mm-inline-media-tests to indicate which message
+ ;; parts are already sufficiently handled by the original
+ ;; presentation of the message in notmuch-show mode. These parts
+ ;; will be inserted directly into the temporary buffer of
+ ;; with-current-notmuch-show-message and silently discarded.
+ ;;
+ ;; Any MIME part not explicitly mentioned here will be handled by an
+ ;; external viewer as configured in the various mailcap files.
(let ((mm-inline-media-tests '(
("text/.*" ignore identity)
("application/pgp-signature" ignore identity)
@@ -183,6 +205,52 @@ indentation."
mm-handle (> (notmuch-count-attachments mm-handle) 1))))
(message "Done"))
+(defun notmuch-show-with-message-as-text (fn)
+ "Apply FN to a text representation of the current message.
+
+FN is called with one argument, the message properties. It should
+operation on the contents of the current buffer."
+
+ ;; Remake the header to ensure that all information is available.
+ (let* ((to (notmuch-show-get-to))
+ (cc (notmuch-show-get-cc))
+ (from (notmuch-show-get-from))
+ (subject (notmuch-show-get-subject))
+ (date (notmuch-show-get-date))
+ (tags (notmuch-show-get-tags))
+ (depth (notmuch-show-get-depth))
+
+ (header (concat
+ "Subject: " subject "\n"
+ "To: " to "\n"
+ (if (not (string= cc ""))
+ (concat "Cc: " cc "\n")
+ "")
+ "From: " from "\n"
+ "Date: " date "\n"
+ (if tags
+ (concat "Tags: "
+ (mapconcat #'identity tags ", ") "\n")
+ "")))
+ (all (buffer-substring (notmuch-show-message-top)
+ (notmuch-show-message-bottom)))
+
+ (props (notmuch-show-get-message-properties)))
+ (with-temp-buffer
+ (insert all)
+ (indent-rigidly (point-min) (point-max) (- depth))
+ ;; Remove the original header.
+ (goto-char (point-min))
+ (re-search-forward "^$" (point-max) nil)
+ (delete-region (point-min) (point))
+ (insert header)
+ (funcall fn props))))
+
+(defun notmuch-show-print-message ()
+ "Print the current message."
+ (interactive)
+ (notmuch-show-with-message-as-text 'notmuch-print-message))
+
(defun notmuch-show-fontify-header ()
(let ((face (cond
((looking-at "[Tt]o:")
@@ -227,21 +295,57 @@ indentation."
"Try to clean a single email ADDRESS for display. Return
unchanged ADDRESS if parsing fails."
(condition-case nil
- (let* ((parsed (mail-header-parse-address address))
- (address (car parsed))
- (name (cdr parsed)))
- ;; Remove double quotes. They might be required during transport,
- ;; but we don't need to see them.
- (when name
- (setq name (replace-regexp-in-string "\"" "" name)))
+ (let (p-name p-address)
+ ;; It would be convenient to use `mail-header-parse-address',
+ ;; but that expects un-decoded mailbox parts, whereas our
+ ;; mailbox parts are already decoded (and hence may contain
+ ;; UTF-8). Given that notmuch should handle most of the awkward
+ ;; cases, some simple string deconstruction should be sufficient
+ ;; here.
+ (cond
+ ;; "User <user@dom.ain>" style.
+ ((string-match "\\(.*\\) <\\(.*\\)>" address)
+ (setq p-name (match-string 1 address)
+ p-address (match-string 2 address)))
+
+ ;; "<user@dom.ain>" style.
+ ((string-match "<\\(.*\\)>" address)
+ (setq p-address (match-string 1 address)))
+
+ ;; Everything else.
+ (t
+ (setq p-address address)))
+
+ (when p-name
+ ;; Remove elements of the mailbox part that are not relevant for
+ ;; display, even if they are required during transport:
+ ;;
+ ;; Backslashes.
+ (setq p-name (replace-regexp-in-string "\\\\" "" p-name))
+
+ ;; Outer single and double quotes, which might be nested.
+ (loop
+ with start-of-loop
+ do (setq start-of-loop p-name)
+
+ when (string-match "^\"\\(.*\\)\"$" p-name)
+ do (setq p-name (match-string 1 p-name))
+
+ when (string-match "^'\\(.*\\)'$" p-name)
+ do (setq p-name (match-string 1 p-name))
+
+ until (string= start-of-loop p-name)))
+
;; If the address is 'foo@bar.com <foo@bar.com>' then show just
;; 'foo@bar.com'.
- (when (string= name address)
- (setq name nil))
-
- (if (not name)
- address
- (concat name " <" address ">")))
+ (when (string= p-name p-address)
+ (setq p-name nil))
+
+ ;; If no name results, return just the address.
+ (if (not p-name)
+ p-address
+ ;; Otherwise format the name and address together.
+ (concat p-name " <" p-address ">")))
(error address)))
(defun notmuch-show-insert-headerline (headers date tags depth)
@@ -278,10 +382,21 @@ message at DEPTH in the current thread."
(run-hooks 'notmuch-show-markup-headers-hook)))))
(define-button-type 'notmuch-show-part-button-type
- 'action 'notmuch-show-part-button-action
+ 'action 'notmuch-show-part-button-default
+ 'keymap 'notmuch-show-part-button-map
'follow-link t
'face 'message-mml)
+(defvar notmuch-show-part-button-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map button-map)
+ (define-key map "s" 'notmuch-show-part-button-save)
+ (define-key map "v" 'notmuch-show-part-button-view)
+ (define-key map "o" 'notmuch-show-part-button-interactively-view)
+ map)
+ "Submap for button commands")
+(fset 'notmuch-show-part-button-map notmuch-show-part-button-map)
+
(defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment)
(let ((button))
(setq button
@@ -296,44 +411,75 @@ message at DEPTH in the current thread."
" ]")
:type 'notmuch-show-part-button-type
:notmuch-part nth
- :notmuch-filename name))
+ :notmuch-filename name
+ :notmuch-content-type content-type))
(insert "\n")
;; return button
button))
;; Functions handling particular MIME parts.
-(defun notmuch-show-save-part (message-id nth &optional filename)
- (let ((process-crypto notmuch-show-process-crypto))
- (with-temp-buffer
- (setq notmuch-show-process-crypto process-crypto)
- ;; Always acquires the part via `notmuch part', even if it is
- ;; available in the JSON output.
- (insert (notmuch-show-get-bodypart-internal message-id nth))
- (let ((file (read-file-name
- "Filename to save as: "
- (or mailcap-download-directory "~/")
- nil nil
- filename)))
- ;; Don't re-compress .gz & al. Arguably we should make
- ;; `file-name-handler-alist' nil, but that would chop
- ;; ange-ftp, which is reasonable to use here.
- (mm-write-region (point-min) (point-max) file nil nil nil 'no-conversion t)))))
+(defmacro notmuch-with-temp-part-buffer (message-id nth &rest body)
+ (declare (indent 2))
+ (let ((process-crypto (make-symbol "process-crypto")))
+ `(let ((,process-crypto notmuch-show-process-crypto))
+ (with-temp-buffer
+ (setq notmuch-show-process-crypto ,process-crypto)
+ ;; Always acquires the part via `notmuch part', even if it is
+ ;; available in the JSON output.
+ (insert (notmuch-show-get-bodypart-internal ,message-id ,nth))
+ ,@body))))
+
+(defun notmuch-show-save-part (message-id nth &optional filename content-type)
+ (notmuch-with-temp-part-buffer message-id nth
+ (let ((file (read-file-name
+ "Filename to save as: "
+ (or mailcap-download-directory "~/")
+ nil nil
+ filename)))
+ ;; Don't re-compress .gz & al. Arguably we should make
+ ;; `file-name-handler-alist' nil, but that would chop
+ ;; ange-ftp, which is reasonable to use here.
+ (mm-write-region (point-min) (point-max) file nil nil nil 'no-conversion t))))
+
+(defun notmuch-show-view-part (message-id nth &optional filename content-type )
+ (notmuch-with-temp-part-buffer message-id nth
+ ;; set mm-inlined-types to nil to force an external viewer
+ (let ((handle (mm-make-handle (current-buffer) (list content-type)))
+ (mm-inlined-types nil))
+ ;; We override mm-save-part as notmuch-show-save-part is better
+ ;; since it offers the filename. We need to lexically bind
+ ;; everything we need for notmuch-show-save-part to prevent
+ ;; potential dynamic shadowing.
+ (lexical-let ((message-id message-id)
+ (nth nth)
+ (filename filename)
+ (content-type content-type))
+ (flet ((mm-save-part (&rest args) (notmuch-show-save-part
+ message-id nth filename content-type)))
+ (mm-display-part handle))))))
+
+(defun notmuch-show-interactively-view-part (message-id nth &optional filename content-type)
+ (notmuch-with-temp-part-buffer message-id nth
+ (let ((handle (mm-make-handle (current-buffer) (list content-type))))
+ (mm-interactively-view-part handle))))
(defun notmuch-show-mm-display-part-inline (msg part nth content-type)
"Use the mm-decode/mm-view functions to display a part in the
current buffer, if possible."
(let ((display-buffer (current-buffer)))
(with-temp-buffer
- (let ((handle (mm-make-handle (current-buffer) (list content-type))))
- (if (and (mm-inlinable-p handle)
- (mm-inlined-p handle))
- (let ((content (notmuch-show-get-bodypart-content msg part nth)))
- (insert content)
- (set-buffer display-buffer)
- (mm-display-part handle)
- t)
- nil)))))
+ (let* ((charset (plist-get part :content-charset))
+ (handle (mm-make-handle (current-buffer) `(,content-type (charset . ,charset)))))
+ ;; If the user wants the part inlined, insert the content and
+ ;; test whether we are able to inline it (which includes both
+ ;; capability and suitability tests).
+ (when (mm-inlined-p handle)
+ (insert (notmuch-show-get-bodypart-content msg part nth))
+ (when (mm-inlinable-p handle)
+ (set-buffer display-buffer)
+ (mm-display-part handle)
+ t))))))
(defvar notmuch-show-multipart/alternative-discouraged
'(
@@ -585,6 +731,10 @@ current buffer, if possible."
nil))
nil))))
+;; Handler for wash generated inline patch fake parts.
+(defun notmuch-show-insert-part-inline-patch-fake-part (msg part content-type nth depth declared-type)
+ (notmuch-show-insert-part-*/* msg part "text/x-diff" nth depth "inline patch"))
+
(defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
;; This handler _must_ succeed - it is the handler of last resort.
(notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
@@ -652,8 +802,8 @@ current buffer, if possible."
;; part, so we make sure that we're down at the end.
(goto-char (point-max))
;; Ensure that the part ends with a carriage return.
- (if (not (bolp))
- (insert "\n")))
+ (unless (bolp)
+ (insert "\n")))
(defun notmuch-show-insert-body (msg body depth)
"Insert the body BODY at depth DEPTH in the current thread."
@@ -715,8 +865,6 @@ current buffer, if possible."
;; compatible with the existing implementation. This just sets it
;; to after the first header.
(notmuch-show-insert-headers headers)
- ;; Headers should include a blank line (backwards compatibility).
- (insert "\n")
(save-excursion
(goto-char content-start)
;; If the subject of this message is the same as that of the
@@ -731,10 +879,12 @@ current buffer, if possible."
(setq notmuch-show-previous-subject bare-subject)
(setq body-start (point-marker))
+ ;; A blank line between the headers and the body.
+ (insert "\n")
(notmuch-show-insert-body msg (plist-get msg :body) depth)
;; Ensure that the body ends with a newline.
- (if (not (bolp))
- (insert "\n"))
+ (unless (bolp)
+ (insert "\n"))
(setq body-end (point-marker))
(setq content-end (point-marker))
@@ -753,6 +903,8 @@ current buffer, if possible."
(overlay-put headers-overlay 'priority 10))
(overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
+ (plist-put msg :depth depth)
+
;; Save the properties for this message. Currently this saves the
;; entire message (augmented it with other stuff), which seems
;; like overkill. We might save a reduced subset (for example, not
@@ -869,17 +1021,14 @@ buffer."
(jit-lock-register #'notmuch-show-buttonise-links)
- ;; Act on visual lines rather than logical lines.
- (visual-line-mode t)
-
(run-hooks 'notmuch-show-hook))
;; Move straight to the first open message
- (if (not (notmuch-show-message-visible-p))
- (notmuch-show-next-open-message))
+ (unless (notmuch-show-message-visible-p)
+ (notmuch-show-next-open-message))
;; Set the header line to the subject of the first open message.
- (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
+ (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-pretty-subject)))
(notmuch-show-mark-read)))
@@ -925,7 +1074,8 @@ thread id. If a prefix is given, crypto processing is toggled."
(define-key map "s" 'notmuch-search)
(define-key map "m" 'notmuch-mua-new-mail)
(define-key map "f" 'notmuch-show-forward-message)
- (define-key map "r" 'notmuch-show-reply)
+ (define-key map "r" 'notmuch-show-reply-sender)
+ (define-key map "R" 'notmuch-show-reply)
(define-key map "|" 'notmuch-show-pipe-message)
(define-key map "w" 'notmuch-show-save-attachments)
(define-key map "V" 'notmuch-show-view-raw-message)
@@ -936,7 +1086,8 @@ thread id. If a prefix is given, crypto processing is toggled."
(define-key map "-" 'notmuch-show-remove-tag)
(define-key map "+" 'notmuch-show-add-tag)
(define-key map "x" 'notmuch-show-archive-thread-then-exit)
- (define-key map "a" 'notmuch-show-archive-thread)
+ (define-key map "a" 'notmuch-show-archive-message-then-next)
+ (define-key map "A" 'notmuch-show-archive-thread-then-next)
(define-key map "N" 'notmuch-show-next-message)
(define-key map "P" 'notmuch-show-previous-message)
(define-key map "n" 'notmuch-show-next-open-message)
@@ -945,6 +1096,7 @@ thread id. If a prefix is given, crypto processing is toggled."
(define-key map " " 'notmuch-show-advance-and-archive)
(define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
(define-key map (kbd "RET") 'notmuch-show-toggle-message)
+ (define-key map "#" 'notmuch-show-print-message)
map)
"Keymap for \"notmuch show\" buffers.")
(fset 'notmuch-show-mode-map notmuch-show-mode-map)
@@ -982,7 +1134,8 @@ All currently available key bindings:
(use-local-map notmuch-show-mode-map)
(setq major-mode 'notmuch-show-mode
mode-name "notmuch-show")
- (setq buffer-read-only t))
+ (setq buffer-read-only t
+ truncate-lines t))
(defun notmuch-show-move-to-message-top ()
(goto-char (notmuch-show-message-top)))
@@ -1104,6 +1257,12 @@ Some useful entries are:
(defun notmuch-show-get-to ()
(notmuch-show-get-header :To))
+(defun notmuch-show-get-depth ()
+ (notmuch-show-get-prop :depth))
+
+(defun notmuch-show-get-pretty-subject ()
+ (notmuch-prettify-subject (notmuch-show-get-subject)))
+
(defun notmuch-show-set-tags (tags)
"Set the tags of the current message."
(notmuch-show-set-prop :tags tags)
@@ -1191,7 +1350,7 @@ thread from the search from which this thread was originally
shown."
(interactive)
(if (notmuch-show-advance)
- (notmuch-show-archive-thread)))
+ (notmuch-show-archive-thread-then-next)))
(defun notmuch-show-rewind ()
"Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
@@ -1218,11 +1377,10 @@ any effects from previous calls to
;; If a small number of lines from the previous message are
;; visible, realign so that the top of the current message is at
;; the top of the screen.
- (if (<= (count-screen-lines (window-start) start-of-message)
- next-screen-context-lines)
- (progn
- (goto-char (notmuch-show-message-top))
- (notmuch-show-message-adjust)))
+ (when (<= (count-screen-lines (window-start) start-of-message)
+ next-screen-context-lines)
+ (goto-char (notmuch-show-message-top))
+ (notmuch-show-message-adjust))
;; Move to the top left of the window.
(goto-char (window-start)))
(t
@@ -1230,9 +1388,14 @@ any effects from previous calls to
(notmuch-show-previous-message)))))
(defun notmuch-show-reply (&optional prompt-for-sender)
- "Reply to the current message."
+ "Reply to the sender and all recipients of the current message."
+ (interactive "P")
+ (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t))
+
+(defun notmuch-show-reply-sender (&optional prompt-for-sender)
+ "Reply to the sender of the current message."
(interactive "P")
- (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender))
+ (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil))
(defun notmuch-show-forward-message (&optional prompt-for-sender)
"Forward the current message."
@@ -1240,14 +1403,19 @@ any effects from previous calls to
(with-current-notmuch-show-message
(notmuch-mua-new-forward-message prompt-for-sender)))
-(defun notmuch-show-next-message ()
- "Show the next message."
- (interactive)
+(defun notmuch-show-next-message (&optional pop-at-end)
+ "Show the next message.
+
+If a prefix argument is given and this is the last message in the
+thread, navigate to the next thread in the parent search buffer."
+ (interactive "P")
(if (notmuch-show-goto-message-next)
(progn
(notmuch-show-mark-read)
(notmuch-show-message-adjust))
- (goto-char (point-max))))
+ (if pop-at-end
+ (notmuch-show-next-thread)
+ (goto-char (point-max)))))
(defun notmuch-show-previous-message ()
"Show the previous message."
@@ -1256,9 +1424,13 @@ any effects from previous calls to
(notmuch-show-mark-read)
(notmuch-show-message-adjust))
-(defun notmuch-show-next-open-message ()
- "Show the next message."
- (interactive)
+(defun notmuch-show-next-open-message (&optional pop-at-end)
+ "Show the next open message.
+
+If a prefix argument is given and this is the last open message
+in the thread, navigate to the next thread in the parent search
+buffer."
+ (interactive "P")
(let (r)
(while (and (setq r (notmuch-show-goto-message-next))
(not (notmuch-show-message-visible-p))))
@@ -1266,10 +1438,12 @@ any effects from previous calls to
(progn
(notmuch-show-mark-read)
(notmuch-show-message-adjust))
- (goto-char (point-max)))))
+ (if pop-at-end
+ (notmuch-show-next-thread)
+ (goto-char (point-max))))))
(defun notmuch-show-previous-open-message ()
- "Show the previous message."
+ "Show the previous open message."
(interactive)
(while (and (notmuch-show-goto-message-previous)
(not (notmuch-show-message-visible-p))))
@@ -1300,7 +1474,7 @@ than only the current message."
(interactive "P\nsPipe message to command: ")
(let (shell-command)
(if entire-thread
- (setq shell-command
+ (setq shell-command
(concat notmuch-command " show --format=mbox "
(shell-quote-argument
(mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
@@ -1407,23 +1581,45 @@ argument, hide all of the messages."
(interactive)
(backward-button 1))
-(defun notmuch-show-archive-thread-internal (show-next)
- ;; Remove the tag from the current set of messages.
+(defun notmuch-show-tag-thread-internal (tag &optional remove)
+ "Add tag to the current set of messages.
+
+If the remove switch is given, tags will be removed instead of
+added."
(goto-char (point-min))
- (loop do (notmuch-show-remove-tag "inbox")
- until (not (notmuch-show-goto-message-next)))
- ;; Move to the next item in the search results, if any.
+ (let ((tag-function (if remove
+ 'notmuch-show-remove-tag
+ 'notmuch-show-add-tag)))
+ (loop do (funcall tag-function tag)
+ until (not (notmuch-show-goto-message-next)))))
+
+(defun notmuch-show-add-tag-thread (tag)
+ "Add tag to all messages in the current thread."
+ (interactive)
+ (notmuch-show-tag-thread-internal tag))
+
+(defun notmuch-show-remove-tag-thread (tag)
+ "Remove tag from all messages in the current thread."
+ (interactive)
+ (notmuch-show-tag-thread-internal tag t))
+
+(defun notmuch-show-next-thread (&optional show-next)
+ "Move to the next item in the search results, if any."
+ (interactive "P")
(let ((parent-buffer notmuch-show-parent-buffer))
(notmuch-kill-this-buffer)
- (if parent-buffer
- (progn
- (switch-to-buffer parent-buffer)
- (forward-line)
- (if show-next
- (notmuch-search-show-thread))))))
+ (when parent-buffer
+ (switch-to-buffer parent-buffer)
+ (notmuch-search-next-thread)
+ (if show-next
+ (notmuch-search-show-thread)))))
+
+(defun notmuch-show-archive-thread (&optional unarchive)
+ "Archive each message in thread.
-(defun notmuch-show-archive-thread ()
- "Archive each message in thread, then show next thread from search.
+If a prefix argument is given, the messages will be
+\"unarchived\" (ie. the \"inbox\" tag will be added instead of
+removed).
Archive each message currently shown by removing the \"inbox\"
tag from each. Then kill this buffer and show the next thread
@@ -1433,13 +1629,39 @@ 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 "P")
+ (if unarchive
+ (notmuch-show-add-tag-thread "inbox")
+ (notmuch-show-remove-tag-thread "inbox")))
+
+(defun notmuch-show-archive-thread-then-next ()
+ "Archive each message in thread, then show next thread from search."
(interactive)
- (notmuch-show-archive-thread-internal t))
+ (notmuch-show-archive-thread)
+ (notmuch-show-next-thread t))
(defun notmuch-show-archive-thread-then-exit ()
"Archive each message in thread, then exit back to search results."
(interactive)
- (notmuch-show-archive-thread-internal nil))
+ (notmuch-show-archive-thread)
+ (notmuch-show-next-thread))
+
+(defun notmuch-show-archive-message (&optional unarchive)
+ "Archive the current message.
+
+If a prefix argument is given, the message will be
+\"unarchived\" (ie. the \"inbox\" tag will be added instead of
+removed)."
+ (interactive "P")
+ (if unarchive
+ (notmuch-show-add-tag "inbox")
+ (notmuch-show-remove-tag "inbox")))
+
+(defun notmuch-show-archive-message-then-next ()
+ "Archive the current message, then show the next open message in the current thread."
+ (interactive)
+ (notmuch-show-archive-message)
+ (notmuch-show-next-open-message t))
(defun notmuch-show-stash-cc ()
"Copy CC field of current message to kill-ring."
@@ -1488,12 +1710,30 @@ buffer."
;; Commands typically bound to buttons.
-(defun notmuch-show-part-button-action (button)
- (let ((nth (button-get button :notmuch-part)))
- (if nth
- (notmuch-show-save-part (notmuch-show-get-message-id) nth
- (button-get button :notmuch-filename))
- (message "Not a valid part (is it a fake part?)."))))
+(defun notmuch-show-part-button-default (&optional button)
+ (interactive)
+ (notmuch-show-part-button-internal button notmuch-show-part-button-default-action))
+
+(defun notmuch-show-part-button-save (&optional button)
+ (interactive)
+ (notmuch-show-part-button-internal button #'notmuch-show-save-part))
+
+(defun notmuch-show-part-button-view (&optional button)
+ (interactive)
+ (notmuch-show-part-button-internal button #'notmuch-show-view-part))
+
+(defun notmuch-show-part-button-interactively-view (&optional button)
+ (interactive)
+ (notmuch-show-part-button-internal button #'notmuch-show-interactively-view-part))
+
+(defun notmuch-show-part-button-internal (button handler)
+ (let ((button (or button (button-at (point)))))
+ (if button
+ (let ((nth (button-get button :notmuch-part)))
+ (if nth
+ (funcall handler (notmuch-show-get-message-id) nth
+ (button-get button :notmuch-filename)
+ (button-get button :notmuch-content-type)))))))
;;
diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el
index 1f420b25..56981d06 100644
--- a/emacs/notmuch-wash.el
+++ b/emacs/notmuch-wash.el
@@ -136,12 +136,13 @@ collapse the remaining lines into a button.")
(lines-count (count-lines (overlay-start overlay) (overlay-end overlay))))
(format label-format lines-count)))
-(defun notmuch-wash-region-to-button (msg beg end type prefix)
+(defun notmuch-wash-region-to-button (msg beg end type &optional prefix)
"Auxiliary function to do the actual making of overlays and buttons
BEG and END are buffer locations. TYPE should a string, either
-\"citation\" or \"signature\". PREFIX is some arbitrary text to
-insert before the button, probably for indentation."
+\"citation\" or \"signature\". Optional PREFIX is some arbitrary
+text to insert before the button, probably for indentation. Note
+that PREFIX should not include a newline."
;; This uses some slightly tricky conversions between strings and
;; symbols because of the way the button code works. Note that
@@ -160,12 +161,15 @@ insert before the button, probably for indentation."
(overlay-put overlay 'type type)
(goto-char (1+ end))
(save-excursion
- (goto-char (1- beg))
- (insert prefix)
- (insert-button (notmuch-wash-button-label overlay)
+ (goto-char beg)
+ (if prefix
+ (insert-before-markers prefix))
+ (let ((button-beg (point)))
+ (insert-before-markers (notmuch-wash-button-label overlay) "\n")
+ (make-button button-beg (1- (point))
'invisibility-spec invis-spec
'overlay overlay
- :type button-type))))
+ :type button-type)))))
(defun notmuch-wash-excerpt-citations (msg depth)
"Excerpt citations and up to one signature."
@@ -177,7 +181,7 @@ insert before the button, probably for indentation."
(msg-end (point-max))
(msg-lines (count-lines msg-start msg-end)))
(notmuch-wash-region-to-button
- msg msg-start msg-end "original" "\n")))
+ msg msg-start msg-end "original")))
(while (and (< (point) (point-max))
(re-search-forward notmuch-wash-citation-regexp nil t))
(let* ((cite-start (match-beginning 0))
@@ -194,7 +198,7 @@ insert before the button, probably for indentation."
(forward-line (- notmuch-wash-citation-lines-suffix))
(notmuch-wash-region-to-button
msg hidden-start (point-marker)
- "citation" "\n")))))
+ "citation")))))
(if (and (not (eobp))
(re-search-forward notmuch-wash-signature-regexp nil t))
(let* ((sig-start (match-beginning 0))
@@ -208,7 +212,7 @@ insert before the button, probably for indentation."
(overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text)
(notmuch-wash-region-to-button
msg sig-start-marker sig-end-marker
- "signature" "\n"))))))
+ "signature"))))))
;;
@@ -290,6 +294,44 @@ When doing so, maintaining citation leaders in the wrapped text."
(defvar diff-file-header-re) ; From `diff-mode.el'.
+(defun notmuch-wash-subject-to-filename (subject &optional maxlen)
+ "Convert a mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\", without the leading patch sequence number
+\"0001-\" and \".patch\" extension. Any leading \"[PREFIX]\"
+style strings are removed prior to conversion.
+
+Optional argument MAXLEN is the maximum length of the resulting
+filename, before trimming any trailing . and - characters."
+ (let* ((s (replace-regexp-in-string "^ *\\(\\[[^]]*\\] *\\)*" "" subject))
+ (s (replace-regexp-in-string "[^A-Za-z0-9._]+" "-" s))
+ (s (replace-regexp-in-string "\\.+" "." s))
+ (s (if maxlen (substring s 0 (min (length s) maxlen)) s))
+ (s (replace-regexp-in-string "[.-]*$" "" s)))
+ s))
+
+(defun notmuch-wash-subject-to-patch-sequence-number (subject)
+ "Convert a patch mail SUBJECT into a patch sequence number.
+
+Return the patch sequence number N from the last \"[PATCH N/M]\"
+style prefix in SUBJECT, or nil if such a prefix can't be found."
+ (when (string-match
+ "^ *\\(\\[[^]]*\\] *\\)*\\[[^]]*?\\([0-9]+\\)/[0-9]+[^]]*\\].*"
+ subject)
+ (string-to-number (substring subject (match-beginning 2) (match-end 2)))))
+
+(defun notmuch-wash-subject-to-patch-filename (subject)
+ "Convert a patch mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\". If the patch mail was generated and sent using
+\"git format-patch/send-email\", this should re-create the
+original filename the sender had."
+ (format "%04d-%s.patch"
+ (or (notmuch-wash-subject-to-patch-sequence-number subject) 1)
+ (notmuch-wash-subject-to-filename subject 52)))
+
(defun notmuch-wash-convert-inline-patch-to-part (msg depth)
"Convert an inline patch into a fake 'text/x-diff' attachment.
@@ -298,27 +340,29 @@ patch and then guesses the extent of the patch, there is scope
for error."
(goto-char (point-min))
- (if (re-search-forward diff-file-header-re nil t)
- (progn
- (beginning-of-line -1)
- (let ((patch-start (point))
- (patch-end (point-max))
- part)
- (goto-char patch-start)
- (if (or
- ;; Patch ends with signature.
- (re-search-forward notmuch-wash-signature-regexp nil t)
- ;; Patch ends with bugtraq comment.
- (re-search-forward "^\\*\\*\\* " nil t))
- (setq patch-end (match-beginning 0)))
- (save-restriction
- (narrow-to-region patch-start patch-end)
- (setq part (plist-put part :content-type "text/x-diff"))
- (setq part (plist-put part :content (buffer-string)))
- (setq part (plist-put part :id -1))
- (setq part (plist-put part :filename "inline patch"))
- (delete-region (point-min) (point-max))
- (notmuch-show-insert-bodypart nil part depth))))))
+ (when (re-search-forward diff-file-header-re nil t)
+ (beginning-of-line -1)
+ (let ((patch-start (point))
+ (patch-end (point-max))
+ part)
+ (goto-char patch-start)
+ (if (or
+ ;; Patch ends with signature.
+ (re-search-forward notmuch-wash-signature-regexp nil t)
+ ;; Patch ends with bugtraq comment.
+ (re-search-forward "^\\*\\*\\* " nil t))
+ (setq patch-end (match-beginning 0)))
+ (save-restriction
+ (narrow-to-region patch-start patch-end)
+ (setq part (plist-put part :content-type "inline-patch-fake-part"))
+ (setq part (plist-put part :content (buffer-string)))
+ (setq part (plist-put part :id -1))
+ (setq part (plist-put part :filename
+ (notmuch-wash-subject-to-patch-filename
+ (plist-get
+ (plist-get msg :headers) :Subject))))
+ (delete-region (point-min) (point-max))
+ (notmuch-show-insert-bodypart nil part depth)))))
;;
diff --git a/emacs/notmuch.el b/emacs/notmuch.el
index fde23779..cd04ffda 100644
--- a/emacs/notmuch.el
+++ b/emacs/notmuch.el
@@ -1,53 +1,54 @@
-; notmuch.el --- run notmuch within emacs
-;
-; Copyright © Carl Worth
-;
-; This file is part of Notmuch.
-;
-; Notmuch 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.
-;
-; Notmuch 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 Notmuch. If not, see <http://www.gnu.org/licenses/>.
-;
-; Authors: Carl Worth <cworth@cworth.org>
-
-; This is an emacs-based interface to the notmuch mail system.
-;
-; You will first need to have the notmuch program installed and have a
-; notmuch database built in order to use this. See
-; http://notmuchmail.org for details.
-;
-; To install this software, copy it to a directory that is on the
-; `load-path' variable within emacs (a good candidate is
-; /usr/local/share/emacs/site-lisp). If you are viewing this from the
-; notmuch source distribution then you can simply run:
-;
-; sudo make install-emacs
-;
-; to install it.
-;
-; Then, to actually run it, add:
-;
-; (require 'notmuch)
-;
-; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
-; or run:
-;
-; emacs -f notmuch
-;
-; Have fun, and let us know if you have any comment, questions, or
-; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
-; required, but is available from http://notmuchmail.org).
+;; notmuch.el --- run notmuch within emacs
+;;
+;; Copyright © Carl Worth
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch 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.
+;;
+;; Notmuch 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 Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+
+;; This is an emacs-based interface to the notmuch mail system.
+;;
+;; You will first need to have the notmuch program installed and have a
+;; notmuch database built in order to use this. See
+;; http://notmuchmail.org for details.
+;;
+;; To install this software, copy it to a directory that is on the
+;; `load-path' variable within emacs (a good candidate is
+;; /usr/local/share/emacs/site-lisp). If you are viewing this from the
+;; notmuch source distribution then you can simply run:
+;;
+;; sudo make install-emacs
+;;
+;; to install it.
+;;
+;; Then, to actually run it, add:
+;;
+;; (require 'notmuch)
+;;
+;; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
+;; or run:
+;;
+;; emacs -f notmuch
+;;
+;; Have fun, and let us know if you have any comment, questions, or
+;; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
+;; required, but is available from http://notmuchmail.org).
(eval-when-compile (require 'cl))
+(require 'crm)
(require 'mm-view)
(require 'message)
@@ -70,17 +71,43 @@ For example:
(setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
\(\"subject\" . \"%s\"\)\)\)"
:type '(alist :key-type (string) :value-type (string))
- :group 'notmuch)
+ :group 'notmuch-search)
(defvar notmuch-query-history nil
"Variable to store minibuffer history for notmuch queries")
-(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+(defun notmuch-tag-completions (&optional prefixes search-terms)
(let ((tag-list
- (with-output-to-string
- (with-current-buffer standard-output
- (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
- (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
+ (split-string
+ (with-output-to-string
+ (with-current-buffer standard-output
+ (apply 'call-process notmuch-command nil t
+ nil "search-tags" search-terms)))
+ "\n+" t)))
+ (if (null prefixes)
+ tag-list
+ (apply #'append
+ (mapcar (lambda (tag)
+ (mapcar (lambda (prefix)
+ (concat prefix tag)) prefixes))
+ tag-list)))))
+
+(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+ (let ((tag-list (notmuch-tag-completions nil search-terms)))
+ (completing-read prompt tag-list)))
+
+(defun notmuch-select-tags-with-completion (prompt &optional prefixes &rest search-terms)
+ (let ((tag-list (notmuch-tag-completions prefixes search-terms))
+ (crm-separator " ")
+ ;; By default, space is bound to "complete word" function.
+ ;; Re-bind it to insert a space instead. Note that <tab>
+ ;; still does the completion.
+ (crm-local-completion-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map crm-local-completion-map)
+ (define-key map " " 'self-insert-command)
+ map)))
+ (delete "" (completing-read-multiple prompt tag-list))))
(defun notmuch-foreach-mime-part (function mm-handle)
(cond ((stringp (car mm-handle))
@@ -139,10 +166,10 @@ This is basically just `format-kbd-macro' but we also convert ESC to M-."
"M-"
(concat desc " "))))
-; I would think that emacs would have code handy for walking a keymap
-; and generating strings for each key, and I would prefer to just call
-; that. But I couldn't find any (could be all implemented in C I
-; suppose), so I wrote my own here.
+;; I would think that emacs would have code handy for walking a keymap
+;; and generating strings for each key, and I would prefer to just call
+;; that. But I couldn't find any (could be all implemented in C I
+;; suppose), so I wrote my own here.
(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
"For a key binding, return a string showing a human-readable
representation of the prefixed key as well as the first line of
@@ -164,16 +191,23 @@ For a mouse binding, return nil."
"\t"
(notmuch-documentation-first-line action))))))
-(defalias 'notmuch-substitute-one-command-key
- (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
+(defun notmuch-substitute-command-keys-one (key)
+ ;; A `keymap' key indicates inheritance from a parent keymap - the
+ ;; inherited mappings follow, so there is nothing to print for
+ ;; `keymap' itself.
+ (when (not (eq key 'keymap))
+ (notmuch-substitute-one-command-key-with-prefix nil key)))
(defun notmuch-substitute-command-keys (doc)
"Like `substitute-command-keys' but with documentation, not function names."
(let ((beg 0))
(while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
- (let ((map (substring doc (match-beginning 1) (match-end 1))))
- (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
- (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
+ (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
+ (keymap (symbol-value (intern keymap-name))))
+ (setq doc (replace-match
+ (mapconcat #'notmuch-substitute-command-keys-one
+ (cdr keymap) "\n")
+ 1 1 doc)))
(setq beg (match-end 0)))
doc))
@@ -192,7 +226,8 @@ For a mouse binding, return nil."
"List of functions to call when notmuch displays the search results."
:type 'hook
:options '(hl-line-mode)
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-hooks)
(defvar notmuch-search-mode-map
(let ((map (make-sparse-keymap)))
@@ -206,7 +241,8 @@ For a mouse binding, return nil."
(define-key map ">" 'notmuch-search-last-thread)
(define-key map "p" 'notmuch-search-previous-thread)
(define-key map "n" 'notmuch-search-next-thread)
- (define-key map "r" 'notmuch-search-reply-to-thread)
+ (define-key map "r" 'notmuch-search-reply-to-thread-sender)
+ (define-key map "R" 'notmuch-search-reply-to-thread)
(define-key map "m" 'notmuch-mua-new-mail)
(define-key map "s" 'notmuch-search)
(define-key map "o" 'notmuch-search-toggle-order)
@@ -262,14 +298,14 @@ For a mouse binding, return nil."
(defun notmuch-search-scroll-down ()
"Move backward through the search results by one window's worth."
(interactive)
- ; I don't know why scroll-down doesn't signal beginning-of-buffer
- ; the way that scroll-up signals end-of-buffer, but c'est la vie.
- ;
- ; So instead of trapping a signal we instead check whether the
- ; window begins on the first line of the buffer and if so, move
- ; directly to that position. (We have to count lines since the
- ; window-start position is not the same as point-min due to the
- ; invisible thread-ID characters on the first line.
+ ;; I don't know why scroll-down doesn't signal beginning-of-buffer
+ ;; the way that scroll-up signals end-of-buffer, but c'est la vie.
+ ;;
+ ;; So instead of trapping a signal we instead check whether the
+ ;; window begins on the first line of the buffer and if so, move
+ ;; directly to that position. (We have to count lines since the
+ ;; window-start position is not the same as point-min due to the
+ ;; invisible thread-ID characters on the first line.
(if (equal (count-lines (point-min) (window-start)) 0)
(goto-char (point-min))
(scroll-down nil)))
@@ -299,27 +335,32 @@ For a mouse binding, return nil."
'((((class color) (background light)) (:background "#f0f0f0"))
(((class color) (background dark)) (:background "#303030")))
"Face for the single-line message summary in notmuch-show-mode."
- :group 'notmuch)
+ :group 'notmuch-show
+ :group 'notmuch-faces)
(defface notmuch-search-date
'((t :inherit default))
"Face used in search mode for dates."
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defface notmuch-search-count
'((t :inherit default))
"Face used in search mode for the count matching the query."
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defface notmuch-search-subject
'((t :inherit default))
"Face used in search mode for subjects."
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defface notmuch-search-matching-authors
'((t :inherit default))
"Face used in search mode for authors matching the query."
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defface notmuch-search-non-matching-authors
'((((class color)
@@ -331,7 +372,8 @@ For a mouse binding, return nil."
(t
(:italic t)))
"Face used in search mode for authors not matching the query."
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defface notmuch-tag-face
'((((class color)
@@ -343,7 +385,8 @@ For a mouse binding, return nil."
(t
(:bold t)))
"Face used in search mode face for tags."
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defun notmuch-search-mode ()
"Major mode displaying results of a notmuch search.
@@ -424,27 +467,27 @@ Complete list of currently available key bindings:
"Display the currently selected thread."
(interactive "P")
(let ((thread-id (notmuch-search-find-thread-id))
- (subject (notmuch-search-find-subject)))
+ (subject (notmuch-prettify-subject (notmuch-search-find-subject))))
(if (> (length thread-id) 0)
(notmuch-show thread-id
(current-buffer)
notmuch-search-query-string
- ;; name the buffer based on notmuch-search-find-subject
- (if (string-match "^[ \t]*$" subject)
- "[No Subject]"
- (truncate-string-to-width
- (concat "*"
- (truncate-string-to-width subject 32 nil nil t)
- "*")
- 32 nil nil t))
+ ;; Name the buffer based on the subject.
+ (concat "*" (truncate-string-to-width subject 30 nil nil t) "*")
crypto-switch)
- (error "End of search results"))))
+ (message "End of search results."))))
(defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
+ "Begin composing a reply-all to the entire current thread in a new buffer."
+ (interactive "P")
+ (let ((message-id (notmuch-search-find-thread-id)))
+ (notmuch-mua-new-reply message-id prompt-for-sender t)))
+
+(defun notmuch-search-reply-to-thread-sender (&optional prompt-for-sender)
"Begin composing a reply to the entire current thread in a new buffer."
(interactive "P")
(let ((message-id (notmuch-search-find-thread-id)))
- (notmuch-mua-new-reply message-id prompt-for-sender)))
+ (notmuch-mua-new-reply message-id prompt-for-sender nil)))
(defun notmuch-call-notmuch-process (&rest args)
"Synchronously invoke \"notmuch\" with the given list of arguments.
@@ -488,7 +531,7 @@ the messages that are about to be tagged"
:type 'hook
:options '(hl-line-mode)
- :group 'notmuch)
+ :group 'notmuch-hooks)
(defcustom notmuch-after-tag-hook nil
"Hooks that are run after tags of a message are modified.
@@ -499,7 +542,7 @@ a list of strings of the form \"+TAG\" or \"-TAG\".
the messages that were tagged"
:type 'hook
:options '(hl-line-mode)
- :group 'notmuch)
+ :group 'notmuch-hooks)
(defun notmuch-search-set-tags (tags)
(save-excursion
@@ -603,7 +646,7 @@ thread or threads in the current region."
This function advances the next thread when finished."
(interactive)
(notmuch-search-remove-tag-thread "inbox")
- (forward-line))
+ (notmuch-search-next-thread))
(defvar notmuch-search-process-filter-data nil
"Data that has not yet been processed.")
@@ -624,17 +667,16 @@ This function advances the next thread when finished."
(goto-char (point-max))
(if (eq status 'signal)
(insert "Incomplete search results (search process was killed).\n"))
- (if (eq status 'exit)
- (progn
- (if notmuch-search-process-filter-data
- (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
- (insert "End of search results.")
- (if (not (= exit-status 0))
- (insert (format " (process returned %d)" exit-status)))
- (insert "\n")
- (if (and atbob
- (not (string= notmuch-search-target-thread "found")))
- (set 'never-found-target-thread t))))))
+ (when (eq status 'exit)
+ (if notmuch-search-process-filter-data
+ (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
+ (insert "End of search results.")
+ (unless (= exit-status 0)
+ (insert (format " (process returned %d)" exit-status)))
+ (insert "\n")
+ (if (and atbob
+ (not (string= notmuch-search-target-thread "found")))
+ (set 'never-found-target-thread t)))))
(when (and never-found-target-thread
notmuch-search-target-line)
(goto-char (point-min))
@@ -655,7 +697,8 @@ attributes overriding earlier. A message having both \"delete\"
and \"unread\" tags with the above settings would have a green
foreground and blue background."
:type '(alist :key-type (string) :value-type (custom-face-edit))
- :group 'notmuch)
+ :group 'notmuch-search
+ :group 'notmuch-faces)
(defun notmuch-search-color-line (start end line-tag-list)
"Colorize lines in `notmuch-show' based on tags."
@@ -806,15 +849,15 @@ non-authors is found, assume that all of the authors match."
(if (/= (match-beginning 1) line)
(insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
(let ((beg (point)))
- (notmuch-search-show-result date count authors subject tags)
+ (notmuch-search-show-result date count authors
+ (notmuch-prettify-subject subject) tags)
(notmuch-search-color-line beg (point) tag-list)
(put-text-property beg (point) 'notmuch-search-thread-id thread-id)
(put-text-property beg (point) 'notmuch-search-authors authors)
(put-text-property beg (point) 'notmuch-search-subject subject)
- (if (string= thread-id notmuch-search-target-thread)
- (progn
- (set 'found-target beg)
- (set 'notmuch-search-target-thread "found"))))
+ (when (string= thread-id notmuch-search-target-thread)
+ (set 'found-target beg)
+ (set 'notmuch-search-target-thread "found")))
(set 'line (match-end 0)))
(set 'more nil)
(while (and (< line (length string)) (= (elt string line) ?\n))
@@ -826,7 +869,7 @@ non-authors is found, assume that all of the authors match."
(goto-char found-target)))
(delete-process proc))))
-(defun notmuch-search-operate-all (action)
+(defun notmuch-search-operate-all (&rest actions)
"Add/remove tags from all matching messages.
This command adds or removes tags from all messages matching the
@@ -837,16 +880,16 @@ will prompt for tags to be added or removed. Tags prefixed with
Each character of the tag name may consist of alphanumeric
characters as well as `_.+-'.
"
- (interactive "sOperation (+add -drop): notmuch tag ")
- (let ((action-split (split-string action " +")))
- ;; Perform some validation
- (let ((words action-split))
- (when (null words) (error "No operation given"))
- (while words
- (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
- (error "Action must be of the form `+thistag -that_tag'"))
- (setq words (cdr words))))
- (apply 'notmuch-tag notmuch-search-query-string action-split)))
+ (interactive (notmuch-select-tags-with-completion
+ "Operations (+add -drop): notmuch tag "
+ '("+" "-")))
+ ;; Perform some validation
+ (when (null actions) (error "No operations given"))
+ (mapc (lambda (action)
+ (unless (string-match-p "^[-+][-+_.[:word:]]+$" action)
+ (error "Action must be of the form `+this_tag' or `-that_tag'")))
+ actions)
+ (apply 'notmuch-tag notmuch-search-query-string actions))
(defun notmuch-search-buffer-title (query)
"Returns the title for a buffer with notmuch search results."
@@ -902,21 +945,25 @@ PROMPT is the string to prompt with."
(t (list string)))))))
;; this was simpler than convincing completing-read to accept spaces:
(define-key keymap (kbd "<tab>") 'minibuffer-complete)
- (read-from-minibuffer prompt nil keymap nil
- 'notmuch-query-history nil nil))))
+ (let ((history-delete-duplicates t))
+ (read-from-minibuffer prompt nil keymap nil
+ 'notmuch-search-history nil nil)))))
;;;###autoload
-(defun notmuch-search (query &optional oldest-first target-thread target-line continuation)
- "Run \"notmuch search\" with the given query string and display results.
+(defun notmuch-search (&optional query oldest-first target-thread target-line continuation)
+ "Run \"notmuch search\" with the given `query' and display results.
-The optional parameters are used as follows:
+If `query' is nil, it is read interactively from the minibuffer.
+Other optional parameters are used as follows:
oldest-first: A Boolean controlling the sort order of returned threads
target-thread: A thread ID (with the thread: prefix) that will be made
current if it appears in the search results.
target-line: The line number to move to if the target thread does not
appear in the search results."
- (interactive (list (notmuch-read-query "Notmuch search: ")))
+ (interactive)
+ (if (null query)
+ (setq query (notmuch-read-query "Notmuch search: ")))
(let ((buffer (get-buffer-create (notmuch-search-buffer-title query))))
(switch-to-buffer buffer)
(notmuch-search-mode)
@@ -990,7 +1037,7 @@ Note that the recommended way of achieving the same is using
:type '(choice (const :tag "notmuch new" nil)
(const :tag "Disabled" "")
(string :tag "Custom script"))
- :group 'notmuch)
+ :group 'notmuch-external)
(defun notmuch-poll ()
"Run \"notmuch new\" or an external script to import mail.
@@ -999,8 +1046,8 @@ Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
depending on the value of `notmuch-poll-script'."
(interactive)
(if (stringp notmuch-poll-script)
- (if (not (string= notmuch-poll-script ""))
- (call-process notmuch-poll-script nil nil))
+ (unless (string= notmuch-poll-script "")
+ (call-process notmuch-poll-script nil nil))
(call-process notmuch-command nil nil nil "new")))
(defun notmuch-search-poll-and-refresh-view ()
@@ -1055,21 +1102,39 @@ current search results AND that are tagged with the given tag."
(interactive)
(notmuch-hello))
+(defun notmuch-interesting-buffer (b)
+ "Is the current buffer of interest to a notmuch user?"
+ (with-current-buffer b
+ (memq major-mode '(notmuch-show-mode
+ notmuch-search-mode
+ notmuch-hello-mode
+ message-mode))))
+
;;;###autoload
-(defun notmuch-jump-to-recent-buffer ()
- "Jump to the most recent notmuch buffer (search, show or hello).
+(defun notmuch-cycle-notmuch-buffers ()
+ "Cycle through any existing notmuch buffers (search, show or hello).
-If no recent buffer is found, run `notmuch'."
+If the current buffer is the only notmuch buffer, bury it. If no
+notmuch buffers exist, run `notmuch'."
(interactive)
- (let ((last
- (loop for buffer in (buffer-list)
- if (with-current-buffer buffer
- (memq major-mode '(notmuch-show-mode
- notmuch-search-mode
- notmuch-hello-mode)))
- return buffer)))
- (if last
- (switch-to-buffer last)
+
+ (let (start first)
+ ;; If the current buffer is a notmuch buffer, remember it and then
+ ;; bury it.
+ (when (notmuch-interesting-buffer (current-buffer))
+ (setq start (current-buffer))
+ (bury-buffer))
+
+ ;; Find the first notmuch buffer.
+ (setq first (loop for buffer in (buffer-list)
+ if (notmuch-interesting-buffer buffer)
+ return buffer))
+
+ (if first
+ ;; If the first one we found is any other than the starting
+ ;; buffer, switch to it.
+ (unless (eq first start)
+ (switch-to-buffer first))
(notmuch))))
(setq mail-user-agent 'notmuch-user-agent)