From 85bfe763362b95935a3967f6585e14b3f9890a70 Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Thu, 5 Oct 2023 08:57:22 +0300 Subject: [PATCH] mu4e-compose: overhaul message composer This is a new composer sub-system, which more closely follows Gnus/message; in particular, we let gnus take care of creating draft messages. This reduces code size and allows us to re-use some of the more advanced features. There are a number of user-visible changes: - `mu4e-compose-resend' directly uses `message-resend', so there is no more editing phase (which used to be a bit broken with re-encoding MIME messages. --- mu4e/meson.build | 1 - mu4e/mu4e-compose.el | 1496 ++++++++++++++++++++-------------------- mu4e/mu4e-draft.el | 738 -------------------- mu4e/mu4e-icalendar.el | 5 +- mu4e/mu4e-obsolete.el | 45 +- mu4e/mu4e.el | 2 +- 6 files changed, 778 insertions(+), 1509 deletions(-) delete mode 100644 mu4e/mu4e-draft.el diff --git a/mu4e/meson.build b/mu4e/meson.build index a2a22bbd..079e7486 100644 --- a/mu4e/meson.build +++ b/mu4e/meson.build @@ -43,7 +43,6 @@ mu4e_srcs=[ 'mu4e-contacts.el', 'mu4e-context.el', 'mu4e-contrib.el', - 'mu4e-draft.el', 'mu4e-folders.el', 'mu4e.el', 'mu4e-headers.el', diff --git a/mu4e/mu4e-compose.el b/mu4e/mu4e-compose.el index 958bca61..75843ab4 100644 --- a/mu4e/mu4e-compose.el +++ b/mu4e/mu4e-compose.el @@ -1,6 +1,6 @@ ;;; mu4e-compose.el --- Compose and send messages -*- lexical-binding: t -*- -;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -22,197 +22,255 @@ ;;; Commentary: -;; In this file, various functions to compose/send messages, piggybacking on -;; gnus' message mode +;; Implements mu4e-compose-mode, which is a `message-mode' derivative. This is a +;; *fairly* thin wrapper around the gnus functions for message composition, +;; integrated with mu4e. Still, quite a bit of code to make it work nicely +;; in the mu4e context. -;; 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 ( .... )) - -;; 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: - -(require 'cl-lib) + (require 'message) -(require 'mail-parse) -(require 'smtpmail) - +(require 'mu4e-obsolete) (require 'mu4e-server) (require 'mu4e-actions) (require 'mu4e-message) -(require 'mu4e-draft) (require 'mu4e-context) (require 'mu4e-window) + -;;; Configuration -;; see mu4e-drafts.el +;;; User configuration for compose-mode +(defgroup mu4e-compose nil + "Customization 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 one 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)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-switch nil + "Where to display the new message? +A symbol: +- nil : default (new buffer) +- window : compose in new window +- frame or t: compose in new frame + +For backward compatibility with `mu4e-compose-in-new-frame', t is +treated as =\\'frame." + :type 'symbol + :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-crypto-policy + '(encrypt-encrypted-replies sign-encrypted-replies) + "Policy to control when messages will be signed/encrypted. + +The value is a list which influence the way draft messages are +created. Specifically, it might contain: + +- `sign-all-messages': Always add a signature. +- `sign-new-messages': Add a signature to new message, ie. + messages that aren't responses to another message. +- `sign-forwarded-messages': Add a signature when forwarding + a message +- `sign-edited-messages': Add a signature to drafts +- `sign-all-replies': Add a signature when responding to + another message. +- `sign-plain-replies': Add a signature when responding to + non-encrypted messages. +- `sign-encrypted-replies': Add a signature when responding + to encrypted messages. + +It should be noted that certain symbols have priorities over one +another. So `sign-all-messages' implies `sign-all-replies', which +in turn implies `sign-plain-replies'. Adding both to the set, is +not a contradiction, but a redundant configuration. + +All `sign-*' options have a `encrypt-*' analogue." + :type '(set :greedy t + (const :tag "Sign all messages" sign-all-messages) + (const :tag "Encrypt all messages" encrypt-all-messages) + (const :tag "Sign new messages" sign-new-messages) + (const :tag "Encrypt new messages" encrypt-new-messages) + (const :tag "Sign forwarded messages" sign-forwarded-messages) + (const :tag "Encrypt forwarded messages" + encrypt-forwarded-messages) + (const :tag "Sign edited messages" sign-edited-messages) + (const :tag "Encrypt edited messages" edited-forwarded-messages) + (const :tag "Sign all replies" sign-all-replies) + (const :tag "Encrypt all replies" encrypt-all-replies) + (const :tag "Sign replies to plain messages" sign-plain-replies) + (const :tag "Encrypt replies to plain messages" + encrypt-plain-replies) + (const :tag "Sign replies to encrypted messages" + sign-encrypted-replies) + (const :tag "Encrypt replies to encrypted messages" + encrypt-encrypted-replies)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-format-flowed nil + "Whether to compose messages to be sent as format=flowed. +\(Or with long lines if variable `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 a symbol, either `reply' or `forward', the +variable `mu4e-compose-parent-message' is the message replied to +/ being forwarded / edited, and `mu4e-compose-type' contains the +type of message to be composed. + +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) + +(defcustom mu4e-compose-reply-ignore-address + '("no-?reply") + "Addresses to prune when doing wide replies. + +This can be a regexp matching the address, a list of regexps or a +predicate function. A value of nil keeps all the addresses." + :type '(choice + (const nil) + function + string + (repeat string)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-dont-reply-to-self nil + "If non-nil, do not include self. +Selfness is decided by `mu4e-personal-address-p'" + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-recipients 'ask + "Which recipients to use when replying to a message. +May be a symbol `ask', `all', `sender'. Note that this option +only applies to non-mailing-list message; for mailing-list +messages, mu4e always asks." + :type '(choice (const ask) + (const all) + (const sender)) + :group 'mu4e-compose) + -;;; Attachments -(defun mu4e-compose-attach-message (msg) - "Insert message MSG as an attachment." - (let ((path (plist-get msg :path))) - (unless (file-exists-p path) - (mu4e-warn "Message file not found")) - (mml-attach-file - path - "message/rfc822" - (or (plist-get msg :subject) "No subject") - "attachment"))) +;;; Runtime variables; useful for user-hooks etc. +(defvar-local mu4e-compose-parent-message nil + "The parent message plist. +This is the message being replied to, forwarded or edited; used +in `mu4e-compose-pre-hook'. For new (non-reply, forward etc.) +messages, it is nil.") -(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")) - (mu4e-compose-attach-message mu4e-captured-message)) +(defvar-local mu4e-compose-type nil + "The compose-type for the current message.") -;;; Misc + +;;; Filenames +(defun mu4e--message-basename() + "Construct a randomized filename for a message with flags FLAGSTR. +It looks something like +