;; mu4e-compose.el -- part of mu4e, the mu mail user agent for emacs ;; ;; Copyright (C) 2011-2012 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 compose/send messages, piggybacking on ;; gnus' message mode ;;; Code: ;; we use some stuff from gnus.. (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) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Composing / Sending messages (defgroup mu4e-compose nil "Customizations for composing/sending messages." :group 'mu4e) (defcustom mu4e-reply-to-address nil "The Reply-To address (if this, for some reason, is not equal to the From: address.)" :type 'string :group 'mu4e-compose) (defcustom mu4e-sent-messages-behavior 'sent "Determines what mu4e does with sent messages - this is a symbol which can be either: '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." :type 'symbol :safe 'symbolp :group 'mu4e-compose) (defcustom mu4e-compose-keep-self-cc nil "Non-nil means your e-mail address is kept on the CC list when replying to messages." :type 'boolean :group 'mu4e-compose) (defvar 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.") (defvar mu4e-compose-parent-message nil "The parent message plist (ie., the message being replied to, forwarded or edited) in `mu4e-compose-pre-hook.") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun mu4e-compose-attach-captured-message() "Insert the last captured message file as an attachment." (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 "application/octet-stream" (or (plist-get mu4e-captured-message :subject) "No subject") "attachment"))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun mu4e~compose-user-agent-construct () "Return the User-Agent string for mu4e. This is either the value of `mu4e-user-agent', or, if not set, a string based on the versions of mu4e and emacs." (format "mu4e %s; emacs %s" mu4e-mu-version emacs-version)) (defun mu4e~compose-cite-original (msg) "Return a cited version of the original message MSG (ie., the plist). This function use gnus' `message-cite-function', and as such all its settings apply." (with-temp-buffer (when (fboundp 'mu4e-view-message-text) ;; keep bytecompiler happy (insert (mu4e-view-message-text msg)) ;; this seems to be needed, otherwise existing signatures ;; won't be stripped (message-yank-original) (goto-char (point-min)) (push-mark (point-max)) (funcall message-cite-function) (pop-mark) (buffer-string)))) (defun mu4e~compose-header (hdr val) "Return a header line of the form HDR: VAL\n. If VAL is nil, return nil." (when val (format "%s: %s\n" hdr val))) (defun mu4e~compose-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 (which may be empty) and the message-id. If the message-id is empty, returns the old References. If both are empty, return nil." (let ((refs (plist-get msg :references)) (old-msgid (plist-get msg :message-id))) (when old-msgid (setq refs (append refs (list old-msgid))) (mapconcat (lambda (msgid) (format "<%s>" msgid)) refs ",")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; determine the recipient fields for new messages (defun mu4e~compose-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>" name email) (format "%s" email)))) lst ", "))) (defun mu4e~compose-address-cell-equal (cell1 cell2) "Return t if cell1 and cell2 have the same e-mail address (case-insensitively), nil otherwise. cell1 and cell2 are cons cells (NAME . EMAIL)." (string= (downcase (or (cdr cell1) "")) (downcase (or (cdr cell2) "")))) (defun mu4e~compose-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)." (let ((reply-to (or (plist-get origmsg :reply-to) (plist-get origmsg :from)))) (delete-duplicates reply-to :test #'mu4e~compose-address-cell-equal))) (defun mu4e~compose-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~compose-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~compose-address-cell-equal cc-cell to-cell)) (mu4e~compose-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) (mu4e~compose-address-cell-equal cc-cell (cons nil user-mail-address))) cc-lst)))) cc-lst))) (defun mu4e~compose-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~compose-recipients-list-to-string (case field (:to (mu4e~compose-create-to-lst origmsg)) (:cc (mu4e~compose-create-cc-lst origmsg reply-all)) (otherwise (mu4e-error "Unsupported field"))))) (defun mu4e~compose-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>" user-full-name user-mail-address) (format "%s" user-mail-address)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun mu4e~compose-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 file. Also see `mu4e-remove-mail-header-separator'." (save-excursion (let ((sepa (propertize mail-header-separator 'intangible t 'read-only "Can't touch this" 'font-lock-face 'mu4e-system-face))) (goto-char (point-min)) ;; search for the first empty line (if (search-forward-regexp "^$" nil t) (replace-match (concat sepa)) (progn ;; no empty line? then prepend one (goto-char (point-max)) (insert "\n" sepa)))))) (defun mu4e~compose-remove-mail-header-separator () "Remove `mail-header-separator; we do this before saving a file (and restore it afterwardds), to ensure that the separator never hits the disk. Also see `mu4e~compose-insert-mail-header-separator." (save-excursion (goto-char (point-min)) ;; remove the --text follows this line-- separator (when (search-forward-regexp (concat "^" mail-header-separator)) (let ((inhibit-read-only t)) (replace-match ""))))) (defun mu4e~compose-user-wants-reply-all (origmsg) "Ask user whether she wants to reply to *all* recipients if there are more than 1 (based on ORIGMSG)." (let* ((recipnum (+ (length (mu4e~compose-create-to-lst origmsg)) (length (mu4e~compose-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~compose-message-filename-construct (&optional flagstr) "Construct a randomized name for a message file with flags FLAGSTR; it looks something like