;;
;; Authors: David Edmondson <dme@dme.org>
-(require 'json)
(require 'message)
(require 'mm-view)
(require 'format-spec)
(eval-when-compile (require 'cl))
+(declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide))
+
;;
(defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook)
:group 'notmuch-send
:group 'notmuch-hooks)
+(defcustom notmuch-mua-compose-in 'current-window
+ (concat
+ "Where to create the mail buffer used to compose a new message.
+Possible values are `current-window' (default), `new-window' and
+`new-frame'. If set to `current-window', the mail buffer will be
+displayed in the current window, so the old buffer will be
+restored when the mail buffer is killed. If set to `new-window'
+or `new-frame', the mail buffer will be displayed in a new
+window/frame that will be destroyed when the buffer is killed.
+You may want to customize `message-kill-buffer-on-exit'
+accordingly."
+ (when (< emacs-major-version 24)
+ " Due to a known bug in Emacs 23, you should not set
+this to `new-window' if `message-kill-buffer-on-exit' is
+disabled: this would result in an incorrect behavior."))
+ :group 'notmuch-send
+ :type '(choice (const :tag "Compose in the current window" current-window)
+ (const :tag "Compose mail in a new window" new-window)
+ (const :tag "Compose mail in a new frame" new-frame)))
+
(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."
:type '(repeat string)
:group 'notmuch-send)
+(defgroup notmuch-reply nil
+ "Replying to messages in notmuch"
+ :group 'notmuch)
+
+(defcustom notmuch-mua-cite-function 'message-cite-original
+ "*Function for citing an original message.
+Predefined functions include `message-cite-original' and
+`message-cite-original-without-signature'.
+Note that these functions use `mail-citation-hook' if that is non-nil."
+ :type '(radio (function-item message-cite-original)
+ (function-item message-cite-original-without-signature)
+ (function-item sc-cite-original)
+ (function :tag "Other"))
+ :link '(custom-manual "(message)Insertion Variables")
+ :group 'notmuch-reply)
+
;;
+(defun notmuch-mua-get-switch-function ()
+ "Get a switch function according to `notmuch-mua-compose-in'."
+ (cond ((eq notmuch-mua-compose-in 'current-window)
+ 'switch-to-buffer)
+ ((eq notmuch-mua-compose-in 'new-window)
+ 'switch-to-buffer-other-window)
+ ((eq notmuch-mua-compose-in 'new-frame)
+ 'switch-to-buffer-other-frame)
+ (t (error "Invalid value for `notmuch-mua-compose-in'"))))
+
+(defun notmuch-mua-maybe-set-window-dedicated ()
+ "Set the selected window as dedicated according to
+`notmuch-mua-compose-in'."
+ (when (or (eq notmuch-mua-compose-in 'new-frame)
+ (eq notmuch-mua-compose-in 'new-window))
+ (set-window-dedicated-p (selected-window) t)))
+
(defun notmuch-mua-user-agent-full ()
"Generate a `User-Agent:' string suitable for notmuch."
(concat (notmuch-mua-user-agent-notmuch)
(defun notmuch-mua-user-agent-notmuch ()
"Generate a `User-Agent:' string suitable for notmuch."
- (concat "Notmuch/" (notmuch-version) " (http://notmuchmail.org)"))
+ (let ((notmuch-version (if (string= notmuch-emacs-version "unknown")
+ (notmuch-cli-version)
+ notmuch-emacs-version)))
+ (concat "Notmuch/" notmuch-version " (http://notmuchmail.org)")))
(defun notmuch-mua-user-agent-emacs ()
"Generate a `User-Agent:' string suitable for notmuch."
(push header message-hidden-headers)))
notmuch-mua-hidden-headers))
+(defun notmuch-mua-reply-crypto (parts)
+ "Add mml sign-encrypt flag if any part of original message is encrypted."
+ (loop for part in parts
+ if (notmuch-match-content-type (plist-get part :content-type) "multipart/encrypted")
+ do (mml-secure-message-sign-encrypt)
+ else if (notmuch-match-content-type (plist-get part :content-type) "multipart/*")
+ do (notmuch-mua-reply-crypto (plist-get part :content))))
+
(defun notmuch-mua-get-quotable-parts (parts)
(loop for part in parts
if (notmuch-match-content-type (plist-get part :content-type) "multipart/alternative")
collect part))
(defun notmuch-mua-insert-quotable-part (message part)
- (save-restriction
- (narrow-to-region (point) (point))
- (notmuch-mm-display-part-inline message part (plist-get part :id)
- (plist-get part :content-type)
- notmuch-show-process-crypto)
- (goto-char (point-max))))
+ ;; We don't want text properties leaking from the show renderer into
+ ;; the reply so we use a temp buffer. Also we don't want hooks, such
+ ;; as notmuch-wash-*, to be run on the quotable part so we set
+ ;; notmuch-show-insert-text/plain-hook to nil.
+ (insert (with-temp-buffer
+ (let ((notmuch-show-insert-text/plain-hook nil))
+ ;; Show the part but do not add buttons.
+ (notmuch-show-insert-bodypart message part 0 'no-buttons))
+ (buffer-substring-no-properties (point-min) (point-max)))))
;; There is a bug in emacs 23's message.el that results in a newline
;; not being inserted after the References header, so the next header
(unless (bolp) (insert "\n")))
(defun notmuch-mua-reply (query-string &optional sender reply-all)
- (let ((args '("reply" "--format=json"))
+ (let ((args '("reply" "--format=sexp" "--format-version=1"))
+ (process-crypto notmuch-show-process-crypto)
reply
original)
- (when notmuch-show-process-crypto
+ (when process-crypto
(setq args (append args '("--decrypt"))))
(if reply-all
(setq args (append args '("--reply-to=sender"))))
(setq args (append args (list query-string)))
- ;; Get the reply object as JSON, and parse it into an elisp object.
- (with-temp-buffer
- (apply 'call-process (append (list notmuch-command nil (list t t) nil) args))
- (goto-char (point-min))
- (let ((json-object-type 'plist)
- (json-array-type 'list)
- (json-false 'nil))
- (setq reply (json-read))))
+ ;; Get the reply object as SEXP, and parse it into an elisp object.
+ (setq reply (apply #'notmuch-call-notmuch-sexp args))
;; Extract the original message to simplify the following code.
(setq original (plist-get reply :original))
collect pair)))
(notmuch-mua-mail (plist-get reply-headers :To)
(plist-get reply-headers :Subject)
- (notmuch-headers-plist-to-alist reply-headers))))
+ (notmuch-headers-plist-to-alist reply-headers)
+ nil (notmuch-mua-get-switch-function))))
;; Insert the message body - but put it in front of the signature
- ;; if one is present
- (goto-char (point-max))
- (if (re-search-backward message-signature-separator nil t)
- (forward-line -1)
- (goto-char (point-max)))
+ ;; if one is present, and after any other content
+ ;; message*setup-hooks may have added to the message body already.
+ (save-restriction
+ (message-goto-body)
+ (narrow-to-region (point) (point-max))
+ (goto-char (point-max))
+ (if (re-search-backward message-signature-separator nil t)
+ (if message-signature-insert-empty-line
+ (forward-line -1))
+ (goto-char (point-max))))
(let ((from (plist-get original-headers :From))
(date (plist-get original-headers :Date))
(start (point)))
- ;; message-cite-original constructs a citation line based on the From and Date
- ;; headers of the original message, which are assumed to be in the buffer.
+ ;; notmuch-mua-cite-function constructs a citation line based
+ ;; on the From and Date headers of the original message, which
+ ;; are assumed to be in the buffer.
(insert "From: " from "\n")
(insert "Date: " date "\n\n")
(set-mark (point))
(goto-char start)
;; Quote the original message according to the user's configured style.
- (message-cite-original))))
+ (funcall notmuch-mua-cite-function)))
- (goto-char (point-max))
+ ;; Crypto processing based crypto content of the original message
+ (when process-crypto
+ (notmuch-mua-reply-crypto (plist-get original :body))))
+
+ ;; Push mark right before signature, if any.
+ (message-goto-signature)
+ (unless (eobp)
+ (end-of-line -1))
(push-mark)
+
(message-goto-body)
(set-buffer-modified-p nil))
-(defun notmuch-mua-forward-message ()
- (message-forward)
+(define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]"
+ "Notmuch message composition mode. Mostly like `message-mode'"
+ (when notmuch-address-command
+ (notmuch-address-setup)))
- (when notmuch-mua-user-agent-function
- (let ((user-agent (funcall notmuch-mua-user-agent-function)))
- (when (not (string= "" user-agent))
- (message-add-header (format "User-Agent: %s" user-agent)))))
- (message-sort-headers)
- (message-hide-headers)
- (set-buffer-modified-p nil)
+(put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify)
- (message-goto-to))
+(define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit)
+(define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send)
(defun notmuch-mua-mail (&optional to subject other-headers &rest other-args)
"Invoke the notmuch mail composition window.
(notmuch-user-name) " <" (notmuch-user-primary-email) ">")) other-headers))
(apply #'message-mail to subject other-headers other-args)
+ (notmuch-message-mode)
+ (notmuch-fcc-header-setup)
(message-sort-headers)
(message-hide-headers)
(set-buffer-modified-p nil)
+ (notmuch-mua-maybe-set-window-dedicated)
(message-goto-to))
(defvar notmuch-mua-sender-history nil)
-(defun notmuch-mua-prompt-for-sender ()
- (interactive)
- (let (name addresses one-name-only)
- ;; If notmuch-identities is non-nil, check if there is a fixed user name.
- (if notmuch-identities
- (let ((components (mapcar 'mail-extract-address-components notmuch-identities)))
- (setq name (caar components)
- addresses (mapcar 'cadr components)
- one-name-only (eval
- (cons 'and
- (mapcar (lambda (identity)
- (string-equal name (car identity)))
- components)))))
- ;; If notmuch-identities is nil, use values from the notmuch configuration file.
- (setq name (notmuch-user-name)
- addresses (cons (notmuch-user-primary-email) (notmuch-user-other-email))
- one-name-only t))
- ;; Now prompt the user, either for an email address only or for a full identity.
- (if one-name-only
- (let ((address
- (ido-completing-read (concat "Sender address for " name ": ") addresses
- nil nil nil 'notmuch-mua-sender-history (car addresses))))
- (concat name " <" address ">"))
- (ido-completing-read "Send mail From: " notmuch-identities
- nil nil nil 'notmuch-mua-sender-history (car notmuch-identities)))))
+;; Workaround: Running `ido-completing-read' in emacs 23.1, 23.2 and 23.3
+;; without some explicit initialization fill freeze the operation.
+;; Hence, we advice `ido-completing-read' to ensure required initialization
+;; is done.
+(if (and (= emacs-major-version 23) (< emacs-minor-version 4))
+ (defadvice ido-completing-read (before notmuch-ido-mode-init activate)
+ (ido-init-completion-maps)
+ (add-hook 'minibuffer-setup-hook 'ido-minibuffer-setup)
+ (add-hook 'choose-completion-string-functions
+ 'ido-choose-completion-string)
+ (ad-disable-advice 'ido-completing-read 'before 'notmuch-ido-mode-init)
+ (ad-activate 'ido-completing-read)))
+(defun notmuch-mua-prompt-for-sender ()
+ "Prompt for a sender from the user's configured identities."
+ (if notmuch-identities
+ (ido-completing-read "Send mail from: " notmuch-identities
+ nil nil nil 'notmuch-mua-sender-history
+ (car notmuch-identities))
+ (let* ((name (notmuch-user-name))
+ (addrs (cons (notmuch-user-primary-email)
+ (notmuch-user-other-email)))
+ (address
+ (ido-completing-read (concat "Sender address for " name ": ") addrs
+ nil nil nil 'notmuch-mua-sender-history
+ (car addrs))))
+ (concat name " <" address ">"))))
+
+(put 'notmuch-mua-new-mail 'notmuch-prefix-doc "... and prompt for sender")
(defun notmuch-mua-new-mail (&optional prompt-for-sender)
- "Invoke the notmuch mail composition window.
+ "Compose new mail.
If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
the From: address first."
(let ((other-headers
(when (or prompt-for-sender notmuch-always-prompt-for-sender)
(list (cons 'From (notmuch-mua-prompt-for-sender))))))
- (notmuch-mua-mail nil nil other-headers)))
+ (notmuch-mua-mail nil nil other-headers nil (notmuch-mua-get-switch-function))))
(defun notmuch-mua-new-forward-message (&optional prompt-for-sender)
"Invoke the notmuch message forwarding window.
+The current buffer must contain an RFC2822 message to forward.
+
If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
the From: address first."
- (interactive "P")
- (if (or prompt-for-sender notmuch-always-prompt-for-sender)
- (let* ((sender (notmuch-mua-prompt-for-sender))
- (address-components (mail-extract-address-components sender))
- (user-full-name (car address-components))
- (user-mail-address (cadr address-components)))
- (notmuch-mua-forward-message))
- (notmuch-mua-forward-message)))
+ (let* ((cur (current-buffer))
+ (message-forward-decoded-p nil)
+ (subject (message-make-forward-subject))
+ (other-headers
+ (when (or prompt-for-sender notmuch-always-prompt-for-sender)
+ (list (cons 'From (notmuch-mua-prompt-for-sender))))))
+ (notmuch-mua-mail nil subject other-headers nil (notmuch-mua-get-switch-function))
+ (message-forward-make-body cur)
+ ;; `message-forward-make-body' shows the User-agent header. Hide
+ ;; it again.
+ (message-hide-headers)
+ (set-buffer-modified-p nil)))
(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all)
- "Invoke the notmuch reply window."
- (interactive "P")
+ "Compose a reply to the message identified by QUERY-STRING.
+
+If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
+the From: address first. If REPLY-ALL is non-nil, the message
+will be addressed to all recipients of the source message."
+
+;; In current emacs (24.3) select-active-regions is set to t by
+;; default. The reply insertion code sets the region to the quoted
+;; message to make it easy to delete (kill-region or C-w). These two
+;; things combine to put the quoted message in the primary selection.
+;;
+;; This is not what the user wanted and is a privacy risk (accidental
+;; pasting of the quoted message). We can avoid some of the problems
+;; by let-binding select-active-regions to nil. This fixes if the
+;; primary selection was previously in a non-emacs window but not if
+;; it was in an emacs window. To avoid the problem in the latter case
+;; we deactivate mark.
+
(let ((sender
(when prompt-for-sender
- (notmuch-mua-prompt-for-sender))))
- (notmuch-mua-reply query-string sender reply-all)))
+ (notmuch-mua-prompt-for-sender)))
+ (select-active-regions nil))
+ (notmuch-mua-reply query-string sender reply-all)
+ (deactivate-mark)))
(defun notmuch-mua-send-and-exit (&optional arg)
(interactive "P")
- (message-send-and-exit arg))
+ (let ((message-fcc-handler-function #'notmuch-fcc-handler))
+ (message-send-and-exit arg)))
+
+(defun notmuch-mua-send (&optional arg)
+ (interactive "P")
+ (let ((message-fcc-handler-function #'notmuch-fcc-handler))
+ (message-send arg)))
(defun notmuch-mua-kill-buffer ()
(interactive)