;; mu4e-draft.el -- part of mu4e, the mu mail user agent for emacs ;; ;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema ;; 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 . ;;; Commentary: ;; In this file, various functions to create draft messages ;; Code (eval-when-compile (byte-compile-disable-warning 'cl-functions)) (require 'cl) (require 'mu4e-vars) (require 'mu4e-utils) (require 'mu4e-message) (require 'message) ;; mail-header-separator ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defcustom mu4e-compose-dont-reply-to-self nil "If non-nil, don't include self (that is, any member of `mu4e-user-mail-address-list') in replies." :type 'boolean :group 'mu4e-compose) (defcustom mu4e-compose-cite-function (or message-cite-function 'message-cite-original-without-signature) "The function to use to cite message in replies and forwarded messages. This is the mu4e-specific version of `message-cite-function'." :type 'function :group 'mu4e-compose) (defcustom mu4e-compose-signature (or message-signature "Sent with my mu4e") "The message signature (i.e. the blob at the bottom of messages). This is the mu4e-specific version of `message-signature'." :group 'mu4e-compose) (defcustom mu4e-compose-signature-auto-include t "Whether to automatically include a message-signature in new messages (if it is set)." :type 'boolean :group 'mu4e-compose) (defcustom mu4e-compose-auto-include-date nil "Whether to include a date header when starting to draft a message; if nil, only do so when sending the message." :type 'boolean :group 'mu4e-compose) (defcustom mu4e-compose-in-new-frame nil "Whether to compose messages in a new frame instead of the current window." :type 'boolean :group 'mu4e-compose) (defvar mu4e-user-agent-string (format "mu4e %s; emacs %s" mu4e-mu-version emacs-version) "The User-Agent string for mu4e.") (defun mu4e~draft-cite-original (msg) "Return a cited version of the original message MSG as a plist. This function uses `mu4e-compose-cite-function', and as such all its settings apply." (with-temp-buffer (when (fboundp 'mu4e-view-message-text) ;; keep bytecompiler happy (let ((mu4e-view-date-format "%Y-%m-%dT%T%z")) (insert (mu4e-view-message-text msg))) (message-yank-original) (goto-char (point-min)) (push-mark (point-max)) ;; set the the signature separator to 'loose', since in the real world, ;; many message don't follow the standard... (let ((message-signature-separator "^-- *$") (message-signature-insert-empty-line t)) (funcall mu4e-compose-cite-function)) (pop-mark) (goto-char (point-min)) (buffer-string)))) (defun mu4e~draft-header (hdr val) "Return a header line of the form \"HDR: VAL\". If VAL is nil, return nil." ;; note: the propertize here is currently useless, since gnus sets its own ;; later. (when val (format "%s: %s\n" (propertize hdr 'face 'mu4e-header-key-face) (propertize val 'face 'mu4e-header-value-face)))) (defconst mu4e~max-reference-num 21 "Maximum number of References:, as suggested by `message-shorten-references'.") (defun mu4e~shorten-1 (list cut surplus) "Cut SURPLUS elements out of LIST, beginning with CUTth one. Code borrowed from `message-shorten-1'." (setcdr (nthcdr (- cut 2) list) (nthcdr (+ (- cut 2) surplus 1) list))) (defun mu4e~draft-references-construct (msg) "Construct the value of the References: header based on MSG as a comma-separated string. Normally, this the concatenation of the existing References + In-Reply-To (which may be empty, an note that :references includes the old in-reply-to as well) and the message-id. If the message-id is empty, returns the old References. If both are empty, return nil." (let* ( ;; these are the ones from the message being replied to / forwarded (refs (mu4e-message-field msg :references)) (msgid (mu4e-message-field msg :message-id)) ;; now, append in (refs (if (and msgid (not (string= msgid ""))) (append refs (list msgid)) refs)) ;; no doubles (refs (delete-duplicates refs :test #'equal)) (refnum (length refs)) (cut 2)) ;; remove some refs when there are too many (when (> refnum mu4e~max-reference-num) (let ((surplus (- refnum mu4e~max-reference-num))) (mu4e~shorten-1 refs cut surplus))) (mapconcat (lambda (id) (format "<%s>" id)) refs " "))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; determine the recipient fields for new messages (defun mu4e~draft-recipients-list-to-string (lst) "Convert a lst LST of address cells into a string with a list of e-mail addresses. If LST is nil, returns nil." (when lst (mapconcat (lambda (addrcell) (let ((name (car addrcell)) (email (cdr addrcell))) (if name (format "%s <%s>" (mu4e~rfc822-quoteit name) email) (format "%s" email)))) lst ", "))) (defun mu4e~draft-address-cell-equal (cell1 cell2) "Return t if CELL1 and CELL2 have the same e-mail address. The comparison is done case-insensitively. If the cells done match return nil. CELL1 and CELL2 are cons cells of the form (NAME . EMAIL)." (string= (downcase (or (cdr cell1) "")) (downcase (or (cdr cell2) "")))) (defun mu4e~draft-create-to-lst (origmsg) "Create a list of address for the To: in a new message, based on the original message ORIGMSG. If the Reply-To address is set, use that, otherwise use the From address. Note, whatever was in the To: field before, goes to the Cc:-list (if we're doing a reply-to-all). Special case: if we were the sender of the original, we simple copy the list form the original." (let ((reply-to (or (plist-get origmsg :reply-to) (plist-get origmsg :from)))) (delete-duplicates reply-to :test #'mu4e~draft-address-cell-equal) (if mu4e-compose-dont-reply-to-self (delete-if (lambda (to-cell) (member-if (lambda (addr) (string= (downcase addr) (downcase (cdr to-cell)))) mu4e-user-mail-address-list)) reply-to) reply-to))) (defun mu4e~draft-create-cc-lst (origmsg reply-all) "Create a list of address for the Cc: in a new message, based on the original message ORIGMSG, and whether it's a reply-all." (when reply-all (let* ((cc-lst ;; get the cc-field from the original, remove dups (delete-duplicates (append (plist-get origmsg :to) (plist-get origmsg :cc)) :test #'mu4e~draft-address-cell-equal)) ;; now we have the basic list, but we must remove ;; addresses also in the to list (cc-lst (delete-if (lambda (cc-cell) (find-if (lambda (to-cell) (mu4e~draft-address-cell-equal cc-cell to-cell)) (mu4e~draft-create-to-lst origmsg))) cc-lst)) ;; finally, we need to remove ourselves from the cc-list ;; unless mu4e-compose-keep-self-cc is non-nil (cc-lst (if (or mu4e-compose-keep-self-cc (null user-mail-address)) cc-lst (delete-if (lambda (cc-cell) (member-if (lambda (addr) (string= (downcase addr) (downcase (cdr cc-cell)))) mu4e-user-mail-address-list)) cc-lst)))) cc-lst))) (defun mu4e~draft-recipients-construct (field origmsg &optional reply-all) "Create value (a string) for the recipient field FIELD (a symbol, :to or :cc), based on the original message ORIGMSG, and (optionally) REPLY-ALL which indicates this is a reply-to-all message. Return nil if there are no recipients for the particular field." (mu4e~draft-recipients-list-to-string (case field (:to (mu4e~draft-create-to-lst origmsg)) (:cc (mu4e~draft-create-cc-lst origmsg reply-all)) (otherwise (mu4e-error "Unsupported field"))))) (defun mu4e~draft-from-construct () "Construct a value for the From:-field of the reply to MSG, based on `user-full-name' and `user-mail-address'; if the latter is nil, function returns nil." (when user-mail-address (if user-full-name (format "%s <%s>" (mu4e~rfc822-quoteit user-full-name) user-mail-address) (format "%s" user-mail-address)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun mu4e~draft-insert-mail-header-separator () "Insert `mail-header-separator' in the first empty line of the message. `message-mode' needs this line to know where the headers end and the body starts. Note, in `mu4e-compose-mode', we use `before-save-hook' and `after-save-hook' to ensure that this separator is never written to the message file. Also see `mu4e-remove-mail-header-separator'." ;; we set this here explicitly, since (as it has happened) a wrong ;; value for this (such as "") breaks address completion and other things (set (make-local-variable 'mail-header-separator) "--text follows this line--") (put 'mail-header-separator 'permanent-local t) (save-excursion ;; make sure there's not one already (mu4e~draft-remove-mail-header-separator) (let ((sepa (propertize mail-header-separator 'intangible t ;; don't make this read-only, message-mode ;; seems to require it being writable in some cases ;;'read-only "Can't touch this" 'rear-nonsticky t 'font-lock-face 'mu4e-compose-separator-face))) (widen) ;; search for the first empty line (goto-char (point-min)) (if (search-forward-regexp "^$" nil t) (replace-match sepa) (progn ;; no empty line? then prepend one (goto-char (point-max)) (insert "\n" sepa)))))) (defun mu4e~draft-remove-mail-header-separator () "Remove `mail-header-separator; we do this before saving a file (and restore it afterwards), to ensure that the separator never hits the disk. Also see `mu4e~draft-insert-mail-header-separator." (save-excursion (widen) (goto-char (point-min)) ;; remove the --text follows this line-- separator (when (search-forward-regexp (concat "^" mail-header-separator) nil t) (let ((inhibit-read-only t)) (replace-match ""))))) (defun mu4e~draft-reply-all-p (origmsg) "Ask user whether she wants to reply to *all* recipients. If there is just one recipient of ORIGMSG do nothing." (let* ((recipnum (+ (length (mu4e~draft-create-to-lst origmsg)) (length (mu4e~draft-create-cc-lst origmsg t)))) (response (if (= recipnum 1) 'all ;; with one recipient, we can reply to 'all'.... (mu4e-read-option "Reply to " `( (,(format "all %d recipients" recipnum) . all) ("sender only" . sender-only)))))) (eq response 'all))) (defun mu4e~draft-message-filename-construct (&optional flagstr) "Construct a randomized name for a message file with flags FLAGSTR. It looks something like