diff options
Diffstat (limited to '_spacemacs.d/local/mu4e/mu4e-compose.el')
-rw-r--r-- | _spacemacs.d/local/mu4e/mu4e-compose.el | 778 |
1 files changed, 778 insertions, 0 deletions
diff --git a/_spacemacs.d/local/mu4e/mu4e-compose.el b/_spacemacs.d/local/mu4e/mu4e-compose.el new file mode 100644 index 0000000..af2b34b --- /dev/null +++ b/_spacemacs.d/local/mu4e/mu4e-compose.el @@ -0,0 +1,778 @@ +;; -*-mode: emacs-lisp; tab-width: 8; indent-tabs-mode: t -*- +;; mu4e-compose.el -- part of mu4e, the mu mail user agent for emacs +;; +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema + +;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> + +;; This file is not part of GNU Emacs. +;; +;; GNU Emacs 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. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; In this file, various functions to compose/send messages, piggybacking on +;; gnus' message mode + + +;; Magic / Rupe Goldberg + +;; 1) When we reply/forward a message, we get it from the backend, ie: +;; we send to the backend (mu4e-compose): +;; compose type:reply docid:30935 +;; backend responds with: +;; (:compose reply :original ( .... <original message> )) + + +;; 2) When we compose a message, message and headers are separated by +;; `mail-header-separator', ie. '--text follows this line--. We use +;; before-save-hook and after-save-hook to remove/re-add this special line, so +;; it stays in the buffer, but never hits the disk. +;; see: +;; mu4e~compose-insert-mail-header-separator +;; mu4e~compose-remove-mail-header-separator +;; +;; (maybe we can get away with remove it only just before sending? what does +;; gnus do?) + +;; 3) When sending a message, we want to do a few things: +;; a) move the message from drafts to the sent folder (maybe; depends on +;; `mu4e-sent-messages-behavior') +;; b) if it's a reply, mark the replied-to message as "R", i.e. replied +;; if it's a forward, mark the forwarded message as "P", i.e. passed (forwarded) +;; c) kill all buffers looking at the sent message + +;; a) is dealt with by message-mode, but we need to tell it where to move the +;; sent message. We do this by adding an Fcc: header with the target folder, +;; see `mu4e~compose-setup-fcc-maybe'. Since message-mode does not natively +;; understand maildirs, we also need to tell it what to do, so we also set +;; `message-fcc-handler-function' there. Finally, we add the the message in +;; the sent-folder to the database. +;; +;; b) this is handled in `mu4e~compose-set-parent-flag' +;; +;; c) this is handled in our handler for the `sent'-message from the backend +;; (`mu4e-sent-handler') +;; + +;;; Code: + +(eval-when-compile (byte-compile-disable-warning 'cl-functions)) +(require 'cl) + +(require 'message) +(require 'mail-parse) +(require 'smtpmail) +(require 'rfc2368) + +(require 'mu4e-utils) +(require 'mu4e-vars) +(require 'mu4e-proc) +(require 'mu4e-actions) +(require 'mu4e-message) +(require 'mu4e-draft) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Composing / Sending messages +(defgroup mu4e-compose nil + "Customizations for composing/sending messages." + :group 'mu4e) + +(defcustom mu4e-sent-messages-behavior 'sent + "Determines what mu4e does with sent messages. + +This is one of the symbols: +* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder') +* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder') +* `delete' delete the sent message. + +Note, when using GMail/IMAP, you should set this to either +`trash' or `delete', since GMail already takes care of keeping +copies in the sent folder. + +Alternatively, `mu4e-sent-messages-behavior' can be a function +which takes no arguments, and which should return on of the mentioned symbols, +for example: + + (setq mu4e-sent-messages-behavior (lambda () + (if (string= (message-sendmail-envelope-from) \"foo@example.com\") + 'delete 'sent))) + +The various `message-' functions from `message-mode' are available +for querying the message information." + :type '(choice (const :tag "move message to mu4e-sent-folder" sent) + (const :tag "move message to mu4e-trash-folder" trash) + (const :tag "delete message" delete)) + :safe 'symbolp + :group 'mu4e-compose) + + +(defcustom mu4e-compose-context-policy 'ask + "Policy for determining the context when composing a new message. + +If the value is `always-ask', ask the user unconditionally. + +In all other cases, if any context matches (using its match +function), this context is used. Otherwise, if none of the +contexts match, we have the following choices: + +- `pick-first': pick the first of the contexts available (ie. the default) +- `ask': ask the user +- `ask-if-none': ask if there is no context yet, otherwise leave it as it is +- nil: return nil; leaves the current context as is. + +Also see `mu4e-context-policy'." + :type '(choice + (const :tag "Always ask what context to use" 'always-ask) + (const :tag "Ask if none of the contexts match" 'ask) + (const :tag "Ask when there's no context yet" 'ask-if-none) + (const :tag "Pick the first context if none match" 'pick-first) + (const :tag "Don't change the context when none match" nil) + :safe 'symbolp + :group 'mu4e-compose)) + +(defcustom mu4e-compose-format-flowed nil + "Whether to compose messages to be sent as format=flowed (or + with long lines if `use-hard-newlines' is set to nil). The + variable `fill-flowed-encode-column' lets you customize the + width beyond which format=flowed lines are wrapped." + :type 'boolean + :safe 'booleanp + :group 'mu4e-compose) + +(defcustom mu4e-compose-pre-hook nil + "Hook run just *before* message composition starts. +If the compose-type is either 'reply' or 'forward', the variable +`mu4e-compose-parent-message' points to the message replied to / +being forwarded / edited. + +Note that there is no draft message yet when this hook runs, it +is meant for influencing the how mu4e constructs the draft +message. If you want to do something with the draft messages after +it has been constructed, `mu4e-compose-mode-hook' would be the +place to do that." + :type 'hook + :group 'mu4e-compose) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defun mu4e-compose-attach-captured-message () + "Insert the last captured message file as an attachment. +Messages are captured with `mu4e-action-capture-message'." + (interactive) + (unless mu4e-captured-message + (mu4e-warn "No message has been captured")) + (let ((path (plist-get mu4e-captured-message :path))) + (unless (file-exists-p path) + (mu4e-warn "Captured message file not found")) + (mml-attach-file + path + "message/rfc822" + (or (plist-get mu4e-captured-message :subject) "No subject") + "attachment"))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +;; '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'. + +(defun mu4e~compose-setup-fcc-maybe () + "Maybe setup Fcc, based on `mu4e-sent-messages-behavior'. +If needed, set the Fcc header, and register the handler function." + (let* ((sent-behavior + ;; Note; we cannot simply use functionp here, since at least + ;; delete is a function, too... + (if (member mu4e-sent-messages-behavior '(delete trash sent)) + mu4e-sent-messages-behavior + (if (functionp mu4e-sent-messages-behavior) + (funcall mu4e-sent-messages-behavior) + mu4e-sent-messages-behavior))) + (mdir + (case sent-behavior + (delete nil) + (trash (mu4e-get-trash-folder mu4e-compose-parent-message)) + (sent (mu4e-get-sent-folder mu4e-compose-parent-message)) + (otherwise + (mu4e-error "unsupported value '%S' `mu4e-sent-messages-behavior'." + mu4e-sent-messages-behavior)))) + (fccfile (and mdir + (concat mu4e-maildir mdir "/cur/" + (mu4e~draft-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 + ;; handler 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 (or maildir "/")))))))) ;; update the database + +(defvar mu4e-compose-hidden-headers + `("^References:" "^Face:" "^X-Face:" + "^X-Draft-From:" "^User-agent:") + "Hidden headers when composing.") + +(defun mu4e~compose-hide-headers () + "Hide the headers as per `mu4e-compose-hidden-headers'." + (let ((message-hidden-headers mu4e-compose-hidden-headers)) + (message-hide-headers))) + +(defconst mu4e~compose-address-fields-regexp + "^\\(To\\|B?Cc\\|Reply-To\\|From\\):") + +(defun mu4e~compose-register-message-save-hooks () + "Just before saving, we remove the mail-header-separator; just +after saving we restore it; thus, the separator should never +appear on disk." + (add-hook 'before-save-hook + 'mu4e~draft-remove-mail-header-separator nil t) + (add-hook 'after-save-hook + (lambda () + (mu4e~compose-set-friendly-buffer-name) + (mu4e~draft-insert-mail-header-separator) + ;; hide some headers again + (mu4e~compose-hide-headers) + (set-buffer-modified-p nil) + (mu4e-message "Saved (%d lines)" (count-lines (point-min) (point-max))) + ;; update the file on disk -- ie., without the separator + (mu4e~proc-add (buffer-file-name) mu4e~draft-drafts-folder)) nil t)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; address completion; inspired by org-contacts.el and +;; https://github.com/nordlow/elisp/blob/master/mine/completion-styles-cycle.el +(defun mu4e~compose-complete-handler (str pred action) + (cond + ((eq action nil) + (try-completion str mu4e~contacts pred)) + ((eq action t) + (all-completions str mu4e~contacts pred)) + ((eq action 'metadata) + ;; our contacts are already sorted - just need to tell the + ;; completion machinery not to try to undo that... + '(metadata + (display-sort-function . mu4e~sort-contacts-for-completion) + (cycle-sort-function . mu4e~sort-contacts-for-completion))))) + +(defun mu4e~compose-complete-contact (&optional start) + "Complete the text at START with a contact. +Ie. either 'name <email>' or 'email')." + (interactive) + (let ((mail-abbrev-mode-regexp mu4e~compose-address-fields-regexp) + (eoh ;; end-of-headers + (save-excursion + (goto-char (point-min)) + (search-forward-regexp mail-header-separator nil t)))) + ;; try to complete only when we're in the headers area, + ;; looking at an address field. + (when (and eoh (> eoh (point)) (mail-abbrev-in-expansion-header-p)) + (let* ((end (point)) + (start + (or start + (save-excursion + (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*") + (goto-char (match-end 0)) + (point))))) + (list start end 'mu4e~compose-complete-handler))))) + +(defun mu4e~compose-setup-completion () + "Set up auto-completion of addresses." + (set (make-local-variable 'completion-ignore-case) t) + (set (make-local-variable 'completion-cycle-threshold) 7) + (add-to-list (make-local-variable 'completion-styles) 'substring) + (add-hook 'completion-at-point-functions + 'mu4e~compose-complete-contact nil t)) + +(defun mu4e~remove-refs-maybe () + "Remove the References: header if the In-Reply-To header is +missing. This allows the user to effectively start a new +message-thread by removing the In-Reply-To header." + (unless (message-fetch-field "in-reply-to") + (message-remove-header "References"))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defvar mu4e-compose-mode-map nil + "Keymap for \"*mu4e-compose*\" buffers.") +(unless mu4e-compose-mode-map + (setq mu4e-compose-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) + (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) + (define-key map (kbd "C-c C-k") 'mu4e-message-kill-buffer) + (define-key map (kbd "M-q") 'mu4e-fill-paragraph) + map))) + +(defun mu4e-fill-paragraph (&optional region) + "If `use-hard-newlines', takes a multi-line paragraph and makes +it into a single line of text. Assume paragraphs are separated +by blank lines. If `use-hard-newlines' is not enabled, this +simply executes `fill-paragraph'." + ;; Inspired by https://www.emacswiki.org/emacs/UnfillParagraph + (interactive (progn (barf-if-buffer-read-only) '(t))) + (if mu4e-compose-format-flowed + (let ((fill-column (point-max)) + (use-hard-newlines nil)); rfill "across" hard newlines + (fill-paragraph nil region)) + (fill-paragraph nil region))) + +(defun mu4e-toggle-use-hard-newlines () + (interactive) + (setq use-hard-newlines (not use-hard-newlines)) + (if use-hard-newlines + (turn-off-auto-fill) + (turn-on-auto-fill))) + + +(defvar mu4e-compose-mode-abbrev-table nil) +(define-derived-mode mu4e-compose-mode message-mode "mu4e:compose" + "Major mode for the mu4e message composition, derived from `message-mode'. +\\{message-mode-map}." + (progn + (use-local-map mu4e-compose-mode-map) + + (set (make-local-variable 'global-mode-string) '(:eval (mu4e-context-label))) + + (set (make-local-variable 'message-signature) mu4e-compose-signature) + ;; set this to allow mu4e to work when gnus-agent is unplugged in gnus + (set (make-local-variable 'message-send-mail-real-function) nil) + (make-local-variable 'message-default-charset) + ;; 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 + ;; useful e.g. when finding an attachment file the directory the current + ;; mail files lives in... + (setq default-directory (expand-file-name "~/")) + + ;; offer completion for e-mail addresses + (when mu4e-compose-complete-addresses + (mu4e~compose-setup-completion)) + + (when mu4e-compose-format-flowed + (turn-off-auto-fill) + (setq truncate-lines nil + word-wrap t + use-hard-newlines t) + ;; Set the marks in the fringes before activating visual-line-mode + (set (make-local-variable 'visual-line-fringe-indicators) + '(left-curly-arrow right-curly-arrow)) + (visual-line-mode t)) + + (define-key-after + (lookup-key message-mode-map [menu-bar text]) + [mu4e-hard-newlines] + '(menu-item "Format=flowed" mu4e-toggle-use-hard-newlines + :button (:toggle . use-hard-newlines) + :help "Toggle format=flowed" + :visible (eq major-mode 'mu4e-compose-mode) + :enable mu4e-compose-format-flowed) + 'sep) + + (define-key-after + (lookup-key mml-mode-map [menu-bar Attachments]) + [mu4e-compose-attach-captured-message] + '(menu-item "Attach captured message" + mu4e-compose-attach-captured-message + :help "Attach message captured in Headers View (with 'a c')" + :visible (eq major-mode 'mu4e-compose-mode)) + (quote Attach\ External...)) + + ;; setup the fcc-stuff, if needed + (add-hook 'message-send-hook + (lambda () ;; mu4e~compose-save-before-sending + ;; when in-reply-to was removed, remove references as well. + (when (eq mu4e~compose-type 'reply) + (mu4e~remove-refs-maybe)) + (when use-hard-newlines + (mu4e-send-harden-newlines)) + ;; for safety, always save the draft before sending + (set-buffer-modified-p t) + (save-buffer) + (mu4e~compose-setup-fcc-maybe) + (widen)) nil t) + ;; when the message has been sent. + (add-hook 'message-sent-hook + (lambda () ;; mu4e~compose-mark-after-sending + (setq mu4e-sent-func 'mu4e-sent-handler) + (mu4e~proc-sent (buffer-file-name) mu4e~draft-drafts-folder)) nil t)) + ;; mark these two hooks as permanent-local, so they'll survive mode-changes + ;; (put 'mu4e~compose-save-before-sending 'permanent-local-hook t) + (put 'mu4e~compose-mark-after-sending 'permanent-local-hook t)) + +(defun mu4e-send-harden-newlines () + "Set the hard property to all newlines." + (save-excursion + (goto-char (point-min)) + (while (search-forward "\n" nil t) + (put-text-property (1- (point)) (point) 'hard t)))) + +(defconst mu4e~compose-buffer-max-name-length 30 + "Maximum length of the mu4e-send-buffer-name.") + +(defvar mu4e~compose-type nil + "Compose-type for this buffer.") + +(defun mu4e~compose-set-friendly-buffer-name (&optional compose-type) + "Set some user-friendly buffer name based on the compose type." + (let* ((subj (message-field-value "subject")) + (subj (unless (and subj (string-match "^[:blank:]*$" subj)) subj)) + (str (or subj + (case compose-type + (reply "*reply*") + (forward "*forward*") + (otherwise "*draft*"))))) + (rename-buffer (generate-new-buffer-name + (truncate-string-to-width str + mu4e~compose-buffer-max-name-length + nil nil t))))) + +(defun* mu4e~compose-handler (compose-type &optional original-msg includes) + "Create a new draft message, or open an existing one. + +COMPOSE-TYPE determines the kind of message to compose and is a +symbol, either `reply', `forward', `edit', `resend' `new'. `edit' +is for editing existing (draft) messages. When COMPOSE-TYPE is +`reply' or `forward', MSG should be a message plist. If +COMPOSE-TYPE is `new', ORIGINAL-MSG should be nil. + +Optionally (when forwarding, replying) ORIGINAL-MSG is the original +message we will forward / reply to. + +Optionally (when forwarding) INCLUDES contains a list of + (:file-name <filename> :mime-type <mime-type> :disposition <disposition>) +for the attachements to include; file-name refers to +a file which our backend has conveniently saved for us (as a +tempfile)." + + ;; Run the hooks defined for `mu4e-compose-pre-hook'. If compose-type is + ;; `reply', `forward' or `edit', `mu4e-compose-parent-message' points to the + ;; message being forwarded or replied to, otherwise it is nil. + (set (make-local-variable 'mu4e-compose-parent-message) original-msg) + (put 'mu4e-compose-parent-message 'permanent-local t) + ;; maybe switch the context + (mu4e~context-autoswitch mu4e-compose-parent-message + mu4e-compose-context-policy) + (run-hooks 'mu4e-compose-pre-hook) + + ;; this opens (or re-opens) a messages with all the basic headers set. + (condition-case nil + (mu4e-draft-open compose-type original-msg) + (quit (kill-buffer) (mu4e-message "Operation aborted") + (return-from mu4e~compose-handler))) + ;; insert mail-header-separator, which is needed by message mode to separate + ;; headers and body. will be removed before saving to disk + (mu4e~draft-insert-mail-header-separator) + ;; include files -- e.g. when forwarding a message with attachments, + ;; we take those from the original. + (save-excursion + (goto-char (point-max)) ;; put attachments at the end + (dolist (att includes) + (mml-attach-file + (plist-get att :file-name) (plist-get att :mime-type)))) + ;; buffer is not user-modified yet + (mu4e~compose-set-friendly-buffer-name compose-type) + (set-buffer-modified-p nil) + ;; now jump to some useful positions, and start writing that mail! + + (if (member compose-type '(new forward)) + (message-goto-to) + (message-goto-body)) + ;; bind to `mu4e-compose-parent-message' of compose buffer + (set (make-local-variable 'mu4e-compose-parent-message) original-msg) + (put 'mu4e-compose-parent-message 'permanent-local t) + ;; remember the compose-type + (set (make-local-variable 'mu4e~compose-type) compose-type) + (put 'mu4e~compose-type 'permanent-local t) + + ;; hide some headers + (mu4e~compose-hide-headers) + ;; switch on the mode + (mu4e-compose-mode) + (when mu4e-compose-in-new-frame + ;; make sure to close the frame when we're done with the message these are + ;; all buffer-local; + (push 'delete-frame message-exit-actions) + (push 'delete-frame message-postpone-actions))) + +(defun mu4e-sent-handler (docid path) + "Handler function, called with DOCID and PATH for the just-sent +message. For Forwarded ('Passed') and Replied messages, try to set +the appropriate flag at the message forwarded or replied-to." + (mu4e~compose-set-parent-flag path) + (when (file-exists-p path) ;; maybe the draft was not saved at all + (mu4e~proc-remove docid)) + ;; kill any remaining buffers for the draft file, or they will hang around... + ;; this seems a bit hamfisted... + (dolist (buf (buffer-list)) + (when (and (buffer-file-name buf) + (string= (buffer-file-name buf) path)) + (if message-kill-buffer-on-exit + (kill-buffer buf)))) + ;; now, try to go back to some previous buffer, in the order + ;; view->headers->main + (if (buffer-live-p mu4e~view-buffer) + (switch-to-buffer mu4e~view-buffer) + (if (buffer-live-p mu4e~headers-buffer) + (switch-to-buffer mu4e~headers-buffer) + ;; if all else fails, back to the main view + (when (fboundp 'mu4e) (mu4e)))) + (mu4e-message "Message sent")) + +(defun mu4e-message-kill-buffer () + "Wrapper around `message-kill-buffer'. +It restores mu4e window layout after killing the compose-buffer." + (interactive) + (let ((current-buffer (current-buffer))) + (message-kill-buffer) + ;; Compose buffer killed + (when (not (equal current-buffer (current-buffer))) + ;; Restore mu4e + (if mu4e-compose-in-new-frame + (delete-frame) + (if (buffer-live-p mu4e~view-buffer) + (switch-to-buffer mu4e~view-buffer) + (if (buffer-live-p mu4e~headers-buffer) + (switch-to-buffer mu4e~headers-buffer) + ;; if all else fails, back to the main view + (when (fboundp 'mu4e) (mu4e)))))))) + +(defun mu4e~compose-set-parent-flag (path) + "Set the 'replied' \"R\" flag on messages we replied to, and the +'passed' \"F\" flag on message we have forwarded. + +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 +have an 'in-reply-to' header, but does have a 'references' header, +it is considered to be a forward message for the message +corresponding with the /last/ message-id in the references header. + +Now, if the message has been determined to be either a forwarded +message or a reply, we instruct the server to update that message +with resp. the 'P' (passed) flag for a forwarded message, or the +'R' flag for a replied message. The original messages are also +marked as Seen. + +Function assumes that it's executed in the context of the message +buffer." + (let ((buf (find-file-noselect path))) + (when buf + (with-current-buffer buf + (message-narrow-to-headers-or-head) + (let ((in-reply-to (message-fetch-field "in-reply-to")) + (forwarded-from) + (references (message-fetch-field "references"))) + (unless in-reply-to + (when references + (with-temp-buffer ;; inspired by `message-shorten-references'. + (insert references) + (goto-char (point-min)) + (let ((refs)) + (while (re-search-forward "<[^ <]+@[^ <]+>" nil t) + (push (match-string 0) refs)) + ;; the last will be the first + (setq forwarded-from (first refs)))))) + ;; remove the <> + (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to)) + (mu4e~proc-move (match-string 1 in-reply-to) nil "+R-N")) + (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from)) + (mu4e~proc-move (match-string 1 forwarded-from) nil "+P-N"))))))) + +(defun mu4e-compose (compose-type) + "Start composing a message of COMPOSE-TYPE, where COMPOSE-TYPE +is a symbol, one of `reply', `forward', `edit', `resend' +`new'. All but `new' take the message at point as input. Symbol +`edit' is only allowed for draft messages." + (let ((msg (mu4e-message-at-point 'noerror))) + ;; some sanity checks + (unless (or msg (eq compose-type 'new)) + (mu4e-warn "No message at point")) + (unless (member compose-type '(reply forward edit resend new)) + (mu4e-error "Invalid compose type '%S'" compose-type)) + (when (and (eq compose-type 'edit) + (not (member 'draft (mu4e-message-field msg :flags)))) + (mu4e-warn "Editing is only allowed for draft messages")) + + ;; 'new is special, since it takes no existing message as arg; therefore, we + ;; don't need to involve the backend, and call the handler *directly* + (if (eq compose-type 'new) + (mu4e~compose-handler 'new) + ;; otherwise, we need the doc-id + (let* ((docid (mu4e-message-field msg :docid)) + ;; decrypt (or not), based on `mu4e-decryption-policy'. + (decrypt + (and (member 'encrypted (mu4e-message-field msg :flags)) + (if (eq mu4e-decryption-policy 'ask) + (yes-or-no-p (mu4e-format "Decrypt message?")) + mu4e-decryption-policy)))) + ;; if there's a visible view window, select that before starting composing + ;; a new message, so that one will be replaced by the compose window. The + ;; 10-or-so line headers buffer is not a good place to write it... + (let ((viewwin (get-buffer-window mu4e~view-buffer))) + (when (window-live-p viewwin) + (select-window viewwin))) + ;; talk to the backend + (mu4e~proc-compose compose-type decrypt docid))))) + +(defun mu4e-compose-reply () + "Compose a reply for the message at point in the headers buffer." + (interactive) + (mu4e-compose 'reply)) + +(defun mu4e-compose-forward () + "Forward the message at point in the headers buffer." + (interactive) + (mu4e-compose 'forward)) + +(defun mu4e-compose-edit () + "Edit the draft message at point in the headers buffer. +This is only possible if the message at point is, in fact, a +draft message." + (interactive) + (mu4e-compose 'edit)) + +(defun mu4e-compose-resend () + "Resend the message at point in the headers buffer." + (interactive) + (mu4e-compose 'resend)) + +(defun mu4e-compose-new () + "Start writing a new message." + (interactive) + (mu4e-compose 'new)) + + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; mu4e-compose-func and mu4e-send-func are wrappers so we can set ourselves +;; as default emacs mailer (define-mail-user-agent etc.) + +;;;###autoload +(defun mu4e~compose-mail (&optional to subject other-headers continue + switch-function yank-action send-actions return-action) + "This is mu4e's implementation of `compose-mail'." + + ;; 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 + (mu4e~compose-handler 'new) + + (when (message-goto-to) ;; reset to-address, if needed + (message-delete-line)) + (message-add-header (concat "To: " to "\n")) + + (when (message-goto-subject) ;; reset subject, if needed + (message-delete-line)) + (message-add-header (concat "Subject: " subject "\n")) + + ;; add any other headers specified + (when other-headers + (message-add-header other-headers)) + + ;; yank message + (if (bufferp yank-action) + (list 'insert-buffer yank-action) + yank-action) + + ;; try to put the user at some reasonable spot... + (if (not to) + (message-goto-to) + (if (not subject) + (message-goto-subject) + (message-goto-body)))) + +;; happily, we can re-use most things from message mode +;;;###autoload +(define-mail-user-agent 'mu4e-user-agent + 'mu4e~compose-mail + 'message-send-and-exit + 'message-kill-buffer + 'message-send-hook) +;; Without this `mail-user-agent' cannot be set to `mu4e-user-agent' +;; through customize, as the custom type expects a function. Not +;; sure whether this function is actually ever used; if it is then +;; returning the symbol is probably the correct thing to do, as other +;; such functions suggest. +(defun mu4e-user-agent () + 'mu4e-user-agent) + +(defun mu4e~compose-browse-url-mail (url &optional ignored) + "Adapter for `browse-url-mailto-function." + (let* ((headers (rfc2368-parse-mailto-url url)) + (to (cdr (assoc "To" headers))) + (subject (cdr (assoc "Subject" headers))) + (body (cdr (assoc "Body" headers)))) + (mu4e~compose-mail to subject) + (if body + (progn + (message-goto-body) + (insert body) + (if (not to) + (message-goto-to) + (if (not subject) + (message-goto-subject) + (message-goto-body))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun mu4e-compose-goto-top () + "Go to the beginning of the message or buffer. +Go to the beginning of the message or, if already there, go to the +beginning of the buffer." + (interactive) + (let ((old-position (point))) + (message-goto-body) + (when (equal (point) old-position) + (goto-char (point-min))))) + +(define-key mu4e-compose-mode-map + (vector 'remap 'beginning-of-buffer) 'mu4e-compose-goto-top) + +(defun mu4e-compose-goto-bottom () + "Go to the end of the message or buffer. +Go to the end of the message (before signature) or, if already there, go to the +end of the buffer." + (interactive) + (let ((old-position (point)) + (message-position (save-excursion (message-goto-body) (point)))) + (goto-char (point-max)) + (when (re-search-backward message-signature-separator message-position t) + (forward-line -1)) + (when (equal (point) old-position) + (goto-char (point-max))))) + +(define-key mu4e-compose-mode-map + (vector 'remap 'end-of-buffer) 'mu4e-compose-goto-bottom) + +(provide 'mu4e-compose) + +;; Load mu4e completely even when this file was loaded through +;; autoload. +(require 'mu4e) |