* mu4e-compose.el: let message-mode FCC take care of saving to sent-messages

- replace the old system with a bit less rube-goldbergesque (only a bit)
    system. Immediate advantage is that attachments are now also present in
    the saved messages, i.e. the copy is now the /same/ as what is sent,
    rather than an approximation.

   'fcc' refers to saving a copy of a sent message to a certain folder. that's
   what these 'Sent mail' folders are for!

   We let message mode take care of this by adding a field
     Fcc: <full-path-to-message-in-target-folder>
   in the "message-send-hook" (ie., just before sending).
   message mode will then take care of the saving when the message is actually
   sent.

   note, where and if you make this copy depends on the value of
    `mu4e-sent-messages-behavior'.

  - also quite a bit of cleanup in particular on mu4e~compose
This commit is contained in:
djcb
2012-04-26 18:08:42 +03:00
parent 084ecc71d2
commit 2f3bd58c03

View File

@ -26,7 +26,7 @@
;; gnus' message mode ;; gnus' message mode
;;; Code: ;;; Code:
;; we use some stuff from gnus.. ;; we use some stuff from gnus..
(require 'cl) (require 'cl)
(require 'message) (require 'message)
(require 'mail-parse) (require 'mail-parse)
@ -69,6 +69,19 @@ sent folder."
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mu4e-compose-attach-captured-message()
"Insert the last captured message file as an attachment."
(interactive)
(unless mu4e-captured-message
(error "No message has been captured"))
(let ((path (plist-get mu4e-captured-message :path)))
(unless (file-exists-p path)
(error "Captured message file not found"))
(mml-attach-file
path
"message/rfc822"
(or (plist-get mu4e-captured-message :subject) "No subject")
"attachment")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -105,7 +118,6 @@ return nil."
(lambda (msgid) (format "<%s>" msgid)) (lambda (msgid) (format "<%s>" msgid))
refs ",")))) refs ","))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; determine the recipient fields for new messages ;; determine the recipient fields for new messages
@ -177,7 +189,7 @@ the original message ORIGMSG, and whether it's a reply-all."
cc-lst)))) cc-lst))))
cc-lst))) cc-lst)))
(defun mu4e~compose-recipients-construct (field origmsg &optional reply-all) (defun mu4e~compose-recipients-construct (field origmsg &optional reply-all)
"Create value (a string) for the recipient field FIELD (a "Create value (a string) for the recipient field FIELD (a
symbol, :to or :cc), based on the original message ORIGMSG, symbol, :to or :cc), based on the original message ORIGMSG,
and (optionally) REPLY-ALL which indicates this is a reply-to-all and (optionally) REPLY-ALL which indicates this is a reply-to-all
@ -202,7 +214,6 @@ nil, function returns nil."
(format "%s" user-mail-address)))) (format "%s" user-mail-address))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mu4e~compose-insert-mail-header-separator () (defun mu4e~compose-insert-mail-header-separator ()
"Insert `mail-header-separator' in the first empty line of the "Insert `mail-header-separator' in the first empty line of the
message. message-mode needs this line to know where the headers end message. message-mode needs this line to know where the headers end
@ -246,6 +257,24 @@ are more than 1 (based on ORIGMSG)."
("sender only")))))) ("sender only"))))))
(= response ?a))) (= response ?a)))
(defun mu4e~compose-message-filename-construct (&optional flagstr)
"Construct a randomized name for a message file with flags FLAGSTR; it looks
something like
<time>-<random>.<hostname>:2,
You can append flags."
(let* ((hostname
(downcase
(save-match-data
(substring system-name
(string-match "^[^.]+" system-name) (match-end 0))))))
(format "%s-%x%x.%s:2,%s"
(format-time-string "%Y%m%d" (current-time))
(emacs-pid) (random t) hostname (or flagstr ""))))
(defun mu4e~compose-common-construct ()
"Construct the common headers for each message."
(mu4e~compose-header "User-agent" (mu4e-user-agent)))
(defun mu4e~compose-reply-construct (origmsg) (defun mu4e~compose-reply-construct (origmsg)
"Create a draft message as a reply to original message ORIGMSG." "Create a draft message as a reply to original message ORIGMSG."
@ -262,9 +291,9 @@ are more than 1 (based on ORIGMSG)."
(mu4e~compose-header "To" (mu4e~compose-recipients-construct :to origmsg)) (mu4e~compose-header "To" (mu4e~compose-recipients-construct :to origmsg))
(mu4e~compose-header "Cc" (mu4e~compose-recipients-construct :cc origmsg (mu4e~compose-header "Cc" (mu4e~compose-recipients-construct :cc origmsg
reply-all)) reply-all))
(mu4e~compose-header "User-agent" (mu4e-user-agent))
(mu4e~compose-header "References" (mu4e~compose-header "References"
(mu4e~compose-refererences-construct origmsg)) (mu4e~compose-refererences-construct origmsg))
(mu4e~compose-common-construct)
(when old-msgid (when old-msgid
(mu4e~compose-header "In-reply-to" (format "<%s>" old-msgid))) (mu4e~compose-header "In-reply-to" (format "<%s>" old-msgid)))
@ -280,14 +309,13 @@ are more than 1 (based on ORIGMSG)."
(defun mu4e~compose-forward-construct (origmsg) (defun mu4e~compose-forward-construct (origmsg)
"Create a draft forward message for original message ORIGMSG." "Create a draft forward message for original message ORIGMSG."
(let ((subject (let ((subject
(or (plist-get origmsg :subject) ""))) (or (plist-get origmsg :subject) "")))
(concat (concat
(mu4e~compose-header "From" (or (mu4e~compose-from-construct) "")) (mu4e~compose-header "From" (or (mu4e~compose-from-construct) ""))
(mu4e~compose-header "Reply-To" mu4e-reply-to-address) (mu4e~compose-header "Reply-To" mu4e-reply-to-address)
(mu4e~compose-header "To" "") (mu4e~compose-header "To" "")
(mu4e~compose-header "User-agent" (mu4e-user-agent)) (mu4e~compose-common-construct)
(mu4e~compose-header "References" (mu4e~compose-header "References"
(mu4e~compose-refererences-construct origmsg)) (mu4e~compose-refererences-construct origmsg))
(mu4e~compose-header "Subject" (mu4e~compose-header "Subject"
@ -305,9 +333,8 @@ are more than 1 (based on ORIGMSG)."
(mu4e~compose-header "From" (or (mu4e~compose-from-construct) "")) (mu4e~compose-header "From" (or (mu4e~compose-from-construct) ""))
(mu4e~compose-header "Reply-To" mu4e-reply-to-address) (mu4e~compose-header "Reply-To" mu4e-reply-to-address)
(mu4e~compose-header "To" "") (mu4e~compose-header "To" "")
(mu4e~compose-header "User-agent" (mu4e-user-agent))
(mu4e~compose-header "Subject" "") (mu4e~compose-header "Subject" "")
"\n")) (mu4e~compose-common-construct)))
(defun mu4e~compose-open-new-draft-file (compose-type &optional msg) (defun mu4e~compose-open-new-draft-file (compose-type &optional msg)
@ -316,18 +343,9 @@ already exist, and optionally fill it with STR. Function also adds
the new message to the database. When the draft message is added to the new message to the database. When the draft message is added to
the database, `mu4e-path-docid-map' will be updated, so that we can the database, `mu4e-path-docid-map' will be updated, so that we can
use the new docid. Returns the full path to the new message." use the new docid. Returns the full path to the new message."
(let* ((hostname (let* ((draft
(downcase
(save-match-data
(substring system-name
(string-match "^[^.]+" system-name) (match-end 0)))))
(draft
(concat mu4e-maildir mu4e-drafts-folder "/cur/" (concat mu4e-maildir mu4e-drafts-folder "/cur/"
;; 'D': rarely used, but hey, it's available (mu4e~compose-message-filename-construct "DS")))
;; 'S': because we're looking at the draft as we speak
(format "%s-%x%x.%s:2,DS"
(format-time-string "%Y%m%d" (current-time))
(emacs-pid) (random t) hostname)))
(str (case compose-type (str (case compose-type
(reply (mu4e~compose-reply-construct msg)) (reply (mu4e~compose-reply-construct msg))
(forward (mu4e~compose-forward-construct msg)) (forward (mu4e~compose-forward-construct msg))
@ -339,52 +357,81 @@ use the new docid. Returns the full path to the new message."
draft)) ;; return the draft buffer file draft)) ;; return the draft buffer file
(define-derived-mode mu4e-compose-mode message-mode "mu4e:compose" ;; 'fcc' refers to saving a copy of a sent message to a certain folder. that's
"Major mode for the mu4e message composition, derived from `message-mode'. ;; what these 'Sent mail' folders are for!
\\{message-mode-map}." ;;
(let ((message-hidden-headers ;; We let message mode take care of this by adding a field
`("^References:" "^Face:" "^X-Face:" "^X-Draft-From:" ;; Fcc: <full-path-to-message-in-target-folder>
"^User-agent:"))) ;; in the "message-send-hook" (ie., just before sending).
(use-local-map mu4e-compose-mode-map) ;; message mode will then take care of the saving when the message is actually
(message-hide-headers) ;; sent.
(make-local-variable 'before-save-hook) ;;
(make-local-variable 'after-save-hook) ;; note, where and if you make this copy depends on the value of
(make-local-variable 'message-default-charset) ;; `mu4e-sent-messages-behavior'.
;; if the default charset is not set, use UTF-8
(unless message-default-charset
(setq message-default-charset 'utf-8))
;; make sure mu4e is started in the background (ie. we don't want to error (defun mu4e~setup-fcc-maybe ()
;; out when sending the message; better to do it now if there's a problem) "Maybe setup Fcc, based on `mu4e-sent-messages-behavior'. If
(mu4e :hide-ui t) needed, set the Fcc header, and register the handler function."
(let* ((mdir
(case mu4e-sent-messages-behavior
(delete nil)
(trash mu4e-trash-folder)
(sent mu4e-sent-folder)
(otherwise
(error "unsupported value '%S' `mu4e-sent-messages-behavior'."
mu4e-sent-messages-behavior))))
(fccfile (and mdir
(concat mu4e-maildir mdir "/cur/"
(mu4e~compose-message-filename-construct "S")))))
;; if there's an fcc header, add it to the file
(when fccfile
(message-add-header (concat "Fcc: " fccfile "\n"))
;; sadly, we cannot define as 'buffer-local'... this will screw up gnus
;; etc. if you run it after mu4e so, (hack hack) we reset it to the old
;; hander after we've done our thing.
(setq message-fcc-handler-function
(lexical-let ((maildir mdir) (old-handler message-fcc-handler-function))
(lambda (file)
(setq message-fcc-handler-function old-handler) ;; reset the fcc handler
(write-file file) ;; writing maildirs files is easy
(mu4e~proc-add file maildir))))))) ;; update the database
;; hack-hack-hack... just before saving, we remove the
;; mail-header-separator; just after saving we restore it; thus, the (defun mu4e~compose-register-message-save-hooks ()
;; separator should never appear on disk "Just before saving, we remove the mail-header-separator; just
(add-hook 'before-save-hook 'mu4e~compose-remove-mail-header-separator) after saving we restore it; thus, the separator should never
appear on disk."
(add-hook 'before-save-hook
'mu4e~compose-remove-mail-header-separator nil t)
(add-hook 'after-save-hook (add-hook 'after-save-hook
(lambda () (lambda ()
(mu4e~compose-set-friendly-buffer-name) (mu4e~compose-set-friendly-buffer-name)
(mu4e~compose-insert-mail-header-separator) (mu4e~compose-insert-mail-header-separator)
(set-buffer-modified-p nil))) (set-buffer-modified-p nil)
;; update the db when the file is saved...] ;; update the file on disk -- ie., without the separator
(add-hook 'after-save-hook (mu4e~proc-add (buffer-file-name) mu4e-drafts-folder)) nil t))
(lambda()
(mu4e~proc-add (buffer-file-name) mu4e-drafts-folder))))
;; notify the backend that a message has been sent. The backend will respond (define-derived-mode mu4e-compose-mode message-mode "mu4e:compose"
;; with (:sent ...) sexp, which is handled in `mu4e~compose-handler'. "Major mode for the mu4e message composition, derived from `message-mode'.
(add-hook 'message-sent-hook \\{message-mode-map}."
(lambda () (let ((message-hidden-headers
(set-buffer-modified-p t) `("^References:" "^Face:" "^X-Face:" "^X-Draft-From:" "^User-agent:")))
(basic-save-buffer) (use-local-map mu4e-compose-mode-map)
(mu4e~proc-sent (buffer-file-name) mu4e-drafts-folder))) (message-hide-headers)
;; register the function; this function will be called when the '(:sent...)' (make-local-variable 'message-default-charset)
;; message is received (see mu4e-proc.el) with parameters docid and path
(setq mu4e-sent-func 'mu4e-sent-handler) ;; if the default charset is not set, use UTF-8
(unless message-default-charset
(setq message-default-charset 'utf-8))
;; make sure mu4e is started in the background (ie. we don't want to error
;; out when sending the message; better to do it now if there's a problem)
(mu4e~start) ;; start mu4e in background, if needed
(mu4e~compose-register-message-save-hooks)
;; set the default directory to the user's home dir; this is probably more ;; set the default directory to the user's home dir; this is probably more
;; useful e.g. when finding an attachment file the directory the current ;; useful e.g. when finding an attachment file the directory the current
;; mail files lives in... ;; mail files lives in...
(setq default-directory (expand-file-name "~/"))) (setq default-directory (expand-file-name "~/"))))
(defconst mu4e~compose-buffer-max-name-length 30 (defconst mu4e~compose-buffer-max-name-length 30
@ -448,6 +495,7 @@ Gnus' `message-mode'."
;; insert mail-header-separator, which is needed by message mode to separate ;; insert mail-header-separator, which is needed by message mode to separate
;; headers and body. will be removed before saving to disk ;; headers and body. will be removed before saving to disk
(mu4e~compose-insert-mail-header-separator) (mu4e~compose-insert-mail-header-separator)
;; include files -- e.g. when forwarding a message with attachments, ;; include files -- e.g. when forwarding a message with attachments,
;; we take those from the original. ;; we take those from the original.
(save-excursion (save-excursion
@ -455,69 +503,51 @@ Gnus' `message-mode'."
(dolist (att includes) (dolist (att includes)
(mml-attach-file (mml-attach-file
(plist-get att :file-name) (plist-get att :mime-type)))) (plist-get att :file-name) (plist-get att :mime-type))))
;; include the message header if it's set; but not when editing an existing ;; include the message header if it's set; but not when editing an existing
;; message. ;; message.
(unless (eq compose-type 'edit) (unless (eq compose-type 'edit)
(when message-signature (when message-signature
(message-insert-signature))) (message-insert-signature)))
;; setup the fcc-stuff, if needed
(add-hook 'message-send-hook
(lambda ()
;; for safety, always save the draft before sending
(set-buffer-modified-p t)
(save-buffer)
(mu4e~setup-fcc-maybe)) nil t)
;; when the message has been sent.
(add-hook 'message-sent-hook
(lambda ()
(setq mu4e-sent-func 'mu4e-sent-handler)
(mu4e~proc-sent (buffer-file-name) mu4e-drafts-folder)) nil t))
;; buffer is not user-modified yet
(mu4e~compose-set-friendly-buffer-name compose-type)
(set-buffer-modified-p nil)
;; now jump to some use positions, and start writing that mail!
(if (member compose-type '(new forward)) (if (member compose-type '(new forward))
(message-goto-to) (message-goto-to)
(message-goto-body)) (message-goto-body)))
(mu4e~compose-set-friendly-buffer-name compose-type)
;; buffer is not user-modified yet
(set-buffer-modified-p nil)))
(defun mu4e-compose-attach-captured-message()
"Insert the last captured message file as an attachment."
(interactive)
(unless mu4e-captured-message
(error "No message has been captured"))
(let ((path (plist-get mu4e-captured-message :path)))
(unless (file-exists-p path)
(error "Captured message file not found"))
(mml-attach-file
path
"message/rfc822"
(or (plist-get mu4e-captured-message :subject) "No subject")
"attachment")))
(defun mu4e-sent-handler (docid path) (defun mu4e-sent-handler (docid path)
"Handler function, called with DOCID and PATH for the just-sent "Handler function, called with DOCID and PATH for the just-sent
message." message."
(with-current-buffer(find-file-noselect path)
;; for Forward ('Passed') and Replied messages, try to set the appropriate ;; for Forward ('Passed') and Replied messages, try to set the appropriate
;; flag at the message forwarded or replied-to ;; flag at the message forwarded or replied-to
(mu4e~compose-set-parent-flag docid path) (mu4e~compose-set-parent-flag path)
;; handle the draft -- should it be moved to the sent-folder, or elsewhere? (kill-buffer) ;; remove the draft
(mu4e~compose-save-copy-maybe docid path) (when (file-exists-p path) ;; maybe the draft was not saved at all
;; now, get rid of the buffer (mu4e~proc-remove docid)))
(kill-buffer)))
(defun mu4e~compose-save-copy-maybe (docid path)
"Handler function, called with DOCID and PATH for the just-sent
message, which will move it to the sent-folder or elsewhere,
depending on the value of `mu4e-sent-messages-behavior'.
Function assumes that it's executed in the context of the message (defun mu4e~compose-set-parent-flag (path)
buffer."
;; first, what to do with the draft message in PATH?
(if (eq mu4e-sent-messages-behavior 'delete)
(mu4e~proc-remove docid) ;; remove it
;; otherwise,
(progn ;; prepare the message for saving
(basic-save-buffer)
;; now either move it to trash or to sent
(if (eq mu4e-sent-messages-behavior 'trash)
(mu4e~proc-move docid mu4e-trash-folder "+T-D+S")
(mu4e~proc-move docid mu4e-sent-folder "-T-D+S")))))
(defun mu4e~compose-set-parent-flag (docid path)
"Set the 'replied' \"R\" flag on messages we replied to, and the "Set the 'replied' \"R\" flag on messages we replied to, and the
'passed' \"F\" flag on message we have forwarded. 'passed' \"F\" flag on message we have forwarded.
If a message has a 'in-reply-to' header, it is considered a reply If a message has an 'in-reply-to' header, it is considered a reply
to the message with the corresponding message id. If it does not to the message with the corresponding message id. If it does not
have an 'in-reply-to' header, but does have a 'references' header, have an 'in-reply-to' header, but does have a 'references' header,
it is considered to be a forward message for the message it is considered to be a forward message for the message
@ -529,8 +559,10 @@ with resp. the 'P' (passed) flag for a forwarded message, or the
'R' flag for a replied message. 'R' flag for a replied message.
Function assumes that it's executed in the context of the message Function assumes that it's executed in the context of the message
buffer. buffer."
" (let ((buf (find-file-noselect path)))
(when buf
(with-current-buffer buf
(let ((in-reply-to (message-fetch-field "in-reply-to")) (let ((in-reply-to (message-fetch-field "in-reply-to"))
(forwarded-from) (forwarded-from)
(references (message-fetch-field "references"))) (references (message-fetch-field "references")))
@ -548,7 +580,7 @@ buffer.
(when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to)) (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
(mu4e~proc-move (match-string 1 in-reply-to) nil "+R")) (mu4e~proc-move (match-string 1 in-reply-to) nil "+R"))
(when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from)) (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
(mu4e~proc-move (match-string 1 forwarded-from) nil "+P")))) (mu4e~proc-move (match-string 1 forwarded-from) nil "+P")))))))
@ -559,21 +591,19 @@ buffer.
(defun mu4e~compose (&optional to subject other-headers continue (defun mu4e~compose (&optional to subject other-headers continue
switch-function yank-action send-actions return-action) switch-function yank-action send-actions return-action)
"mu4e's implementation of `compose-mail'." "mu4e's implementation of `compose-mail'."
;; create a new draft message 'resetting' (as below) is not actually needed in ;; create a new draft message 'resetting' (as below) is not actually needed in
;; this case, but let's prepare for the re-edit case as well ;; this case, but let's prepare for the re-edit case as well
(mu4e~compose-handler 'new) (mu4e~compose-handler 'new)
(when (message-goto-to) ;; reset to-address, if needed (when (message-goto-to) ;; reset to-address, if needed
(message-delete-line)) (message-delete-line))
(insert (concat "To: " to "\n")) (message-add-header (concat "To: " to "\n"))
(when (message-goto-subject) ;; reset subject, if needed (when (message-goto-subject) ;; reset subject, if needed
(message-delete-line)) (message-delete-line))
(insert (concat "Subject: " subject "\n")) (message-add-header (concat "Subject: " subject "\n"))
;; add any other headers specified; FIXME: for some reason, these appear ;; add any other headers specified
;; before any other headers
(when other-headers (when other-headers
(message-add-header other-headers)) (message-add-header other-headers))
@ -595,6 +625,6 @@ buffer.
'message-send-and-exit 'message-send-and-exit
'message-kill-buffer 'message-kill-buffer
'message-send-hook) 'message-send-hook)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'mu4e-compose) (provide 'mu4e-compose)