mu/emacs/mu4e-send.el

423 lines
15 KiB
EmacsLisp
Raw Normal View History

2011-12-13 08:07:38 +01:00
;; mu4e-send.el -- part of mm, the mu mail user agent
;;
;; Copyright (C) 2011 Dirk-Jan C. Binnema
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Keywords: email
;; Version: 0.0
;; 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
2011-10-10 07:38:14 +02:00
;; gnus' message mode
;; mm
;;; Code:
(eval-when-compile (require 'cl))
;; we use some stuff from gnus...
(require 'message)
(require 'mail-parse)
;; internal variables / constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2011-12-13 08:07:38 +01:00
(defconst mu4e-msg-draft-name "*mu4e-draft*"
"Name for draft messages.")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FIXME
2011-12-13 08:07:38 +01:00
(defun mu4e-mu-binary-version () "0.98pre")
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-user-agent ()
"Return the User-Agent string for mm. This is either the value
2011-12-13 08:07:38 +01:00
of `mu4e-user-agent', or, if not set, a string based on the
version of mm and emacs."
2011-12-13 08:07:38 +01:00
(or mu4e-user-agent
(format "mu %s; emacs %s" (mu4e-mu-binary-version) emacs-version)))
2011-12-13 08:07:38 +01:00
(defun mu4e-view-body (msg)
"Get the body for this message, which is either :body-txt,
or if not available, :body-html converted to text)."
(or (plist-get msg :body-txt)
(with-temp-buffer
(plist-get msg :body-html)
(html2text)
(buffer-string))
"No body found"))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-cite-original (msg)
"Cite the body text of MSG, with a \"On %s, %s wrote:\"
line (with the %s's replaced with the date of MSG and the name
or e-mail address of its sender (or 'someone' if nothing
else)), followed of the quoted body of MSG, constructed by by
2011-12-13 08:07:38 +01:00
prepending `mu4e-msg-citation-prefix' to each line. If there is
no body in MSG, return nil."
(let* ((from (plist-get msg :from))
;; first try plain-text, then html
(body (or (plist-get msg :body-txt)
(with-temp-buffer
(plist-get msg :body-html)
(html2text)
2011-11-09 07:35:24 +01:00
(buffer-string))))
(body (and body (replace-regexp-in-string "[\r\240]" " " body))))
(when body
(concat
(format "On %s, %s wrote:"
(format-time-string "%c" (plist-get msg :date))
(if (and from (car from)) ;; a list ((<name> . <email>))
(or (caar from) (cdar from) "someone")
"someone"))
"\n\n"
(replace-regexp-in-string "^" " > " body)))))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-recipients-remove (lst email-to-remove)
"Remove the recipient with EMAIL from the recipient list (of form
'( (\"A\" . \"a@example.com\") (\"B\" . \"B@example.com\"))."
(remove-if
(lambda (name-email)
(let ((email (cdr name-email)))
(when email (string= email-to-remove (downcase email))))) lst))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-recipients-to-string (lst)
"Convert a recipient list (of form '( (\"A\"
. \"a@example.com\") (\"B\" . \"B@example.com\") (nil
. \"c@example.com\")) into a string of form \"A <@aexample.com>, B
<b@example.com>, c@example.com\."
2011-11-09 07:55:39 +01:00
(when lst
(mapconcat
(lambda (recip)
(let ((name (car recip)) (email (cdr recip)))
(if name
(format "%s <%s>" name email)
(format "%s" email)))) lst ", ")))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-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)))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-references-create (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))
2011-10-02 20:35:03 +02:00
(old-msgid (plist-get msg :message-id)))
(when old-msgid
(setq refs (append refs (list old-msgid)))
(mapconcat
(lambda (msgid) (format "<%s>" msgid))
refs ","))))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-to-create (msg)
"Construct the To: header for a reply-message based on some
2011-11-09 07:55:39 +01:00
message MSG. This takes the Reply-To address of MSG if it exist, or
2011-11-09 07:35:24 +01:00
the From:-address otherwise. The result is either nil or a string
2011-11-09 07:55:39 +01:00
which can be used for the To:-field. Note, when it's present,
Reply-To contains a string of one or more addresses,
comma-separated."
(or
(plist-get msg :reply-to)
2011-12-13 08:07:38 +01:00
(mu4e-msg-recipients-to-string (plist-get msg :from))))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-cc-create (msg reply-all)
"Get the list of Cc-addresses for the reply to MSG. If REPLY-ALL
2011-11-09 07:35:24 +01:00
is nil this is simply empty, otherwise it is the old CC-list
together with the old TO-list, minus `user-mail-address'. The
result of this function is either nil or a string to be used for
the Cc: field."
(let ((cc-lst (plist-get msg :cc))
(to-lst (plist-get msg :to)))
(when reply-all
(setq cc-lst (append cc-lst to-lst)))
;; remove myself from cc
2011-12-13 08:07:38 +01:00
(setq cc-lst (mu4e-msg-recipients-remove cc-lst user-mail-address))
(mu4e-msg-recipients-to-string cc-lst)))
2011-11-09 07:35:24 +01:00
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-from-create ()
"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))))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-create-reply (msg)
2011-10-02 20:35:03 +02:00
"Create a draft message as a reply to MSG.
A reply message has fields:
From: - see `mu-msg-from-create'
2011-12-13 08:07:38 +01:00
To: - see `mu4e-msg-to-create'
Cc: - see `mu4e-msg-cc-create'
Subject: - `mu4e-msg-reply-prefix' + subject of MSG
then, the following fields, normally hidden from user:
Reply-To: - if `mail-reply-to' has been set
2011-12-13 08:07:38 +01:00
References: - see `mu4e-msg-references-create'
In-Reply-To: - message-id of MSG
2011-12-13 08:07:38 +01:00
User-Agent - see `mu4e-msg-user-agent'
2011-09-30 07:37:47 +02:00
Then follows `mail-header-separator' (for `message-mode' to separate
body from headers)
2011-12-13 08:07:38 +01:00
And finally, the cited body of MSG, as per `mu4e-msg-cite-original'."
2011-10-02 20:35:03 +02:00
(let* ((recipnum (+ (length (plist-get msg :to))
(length (plist-get msg :cc))))
(reply-all (when (> recipnum 1)
(yes-or-no-p
(format "Reply to all ~%d recipients? "
(+ recipnum)))))
(old-msgid (plist-get msg :message-id))
(subject (plist-get msg :subject)))
2011-10-02 20:35:03 +02:00
(concat
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "From" (or (mu4e-msg-from-create) ""))
2011-10-02 20:35:03 +02:00
(when (boundp 'mail-reply-to)
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "Reply-To" mail-reply-to))
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "To" (or (mu4e-msg-to-create msg) ""))
(mu4e-msg-header "Cc" (mu4e-msg-cc-create msg reply-all))
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "User-agent" (mu4e-msg-user-agent))
(mu4e-msg-header "References" (mu4e-msg-references-create msg))
(when old-msgid
(mu4e-msg-header "In-reply-to" (format "<%s>" old-msgid)))
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "Subject"
(concat mu4e-msg-reply-prefix (if subject subject "")))
2011-09-30 07:37:47 +02:00
2011-10-02 20:35:03 +02:00
(propertize mail-header-separator 'read-only t 'intangible t) '"\n"
2011-10-10 07:38:14 +02:00
"\n\n"
2011-12-13 08:07:38 +01:00
(mu4e-msg-cite-original msg))))
2011-09-30 07:37:47 +02:00
;; TODO: attachments
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-create-forward (msg)
"Create a draft forward message for MSG.
A forward message has fields:
2011-12-13 08:07:38 +01:00
From: - see `mu4e-msg-from-create'
To: - empty
2011-12-13 08:07:38 +01:00
Subject: - `mu4e-msg-forward-prefix' + subject of MSG
then, the following fields, normally hidden from user:
Reply-To: - if `mail-reply-to' has been set
2011-12-13 08:07:38 +01:00
References: - see `mu4e-msg-references-create'
User-Agent - see `mu4e-msg-user-agent'
2011-09-30 07:37:47 +02:00
Then follows `mail-header-separator' (for `message-mode' to separate
body from headers)
2011-12-13 08:07:38 +01:00
And finally, the cited body of MSG, as per `mu4e-msg-cite-original'."
(concat
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "From" (or (mu4e-msg-from-create) ""))
(when (boundp 'mail-reply-to)
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "Reply-To" mail-reply-to))
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "To" "")
(mu4e-msg-header "User-agent" (mu4e-msg-user-agent))
(mu4e-msg-header "References" (mu4e-msg-references-create msg))
(mu4e-msg-header"Subject"
(concat mu4e-msg-forward-prefix (plist-get msg :subject)))
2011-09-30 07:37:47 +02:00
(propertize mail-header-separator 'read-only t 'intangible t) "\n"
2011-10-10 07:38:14 +02:00
"\n\n"
2011-12-13 08:07:38 +01:00
(mu4e-msg-cite-original msg)))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-create-new ()
"Create a new message.
A new draft message has fields:
From: - see `mu-msg-from-create'
To: - empty
Subject: - empty
then, the following fields, normally hidden from user:
Reply-To: - if `mail-reply-to' has been set
2011-12-13 08:07:38 +01:00
User-Agent - see `mu4e-msg-user-agent'
2011-09-30 07:37:47 +02:00
Then follows `mail-header-separator' (for `message-mode' to separate
body from headers)."
(concat
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "From" (or (mu4e-msg-from-create) ""))
(when (boundp 'mail-reply-to)
2011-12-13 08:07:38 +01:00
(mu4e-msg-header "Reply-To" mail-reply-to))
(mu4e-msg-header "To" "")
(mu4e-msg-header "User-agent" (mu4e-msg-user-agent))
(mu4e-msg-header "Subject" "")
2011-09-30 07:37:47 +02:00
(propertize mail-header-separator 'read-only t 'intangible t) "\n"))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-open-draft (compose-type &optional msg)
2011-10-02 20:35:03 +02:00
"Open a draft file for a new message, creating it if it does not
already exist, and optionally fill it with STR. Function also adds
the new message to the database. When the draft message is added to
2011-12-13 08:07:38 +01:00
the database, `mu4e-path-docid-map' will be updated, so that we can
2011-11-05 09:26:24 +01:00
use the new docid. Returns the full path to the new message."
(let* ((hostname
2011-11-09 07:55:39 +01:00
(downcase
2011-11-05 09:26:24 +01:00
(save-match-data
(substring system-name
(string-match "^[^.]+" system-name) (match-end 0)))))
(draft
2011-12-13 08:07:38 +01:00
(concat mu4e-maildir mu4e-drafts-folder "/cur/"
2011-11-05 09:26:24 +01:00
(format "%s-%x%x.%s:2,D" ;; 'D': rarely used, but hey, it's available
(format-time-string "%Y%m%d" (current-time))
(emacs-pid) (random t) hostname)))
(str (case compose-type
2011-12-13 08:07:38 +01:00
(reply (mu4e-msg-create-reply msg))
(forward (mu4e-msg-create-forward msg))
(new (mu4e-msg-create-new))
2011-11-05 09:26:24 +01:00
(t (error "unsupported compose-type %S" compose-type)))))
2011-10-02 20:35:03 +02:00
(when str
(with-temp-file draft
(insert str)
(write-file draft)))
;; save our file immediately, add add it to the db; thus, we can retrieve
2011-12-13 08:07:38 +01:00
;; the new docid from `mu4e-path-docid-map'.
(mu4e-proc-add draft mu4e-drafts-folder)
2011-10-02 20:35:03 +02:00
draft))
2011-12-13 08:07:38 +01:00
(defun mu4e-send-compose-handler (compose-type &optional msg)
2011-10-02 20:35:03 +02:00
"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', `new'. `edit' is for
editing existing messages.
When COMPOSE-TYPE is `reply' or `forward', MSG should be a message
plist. If COMPOSE-TYPE is `new', MSG should be nil.
The name of the draft folder is constructed from the concatenation
2011-12-13 08:07:38 +01:00
of `mu4e-maildir' and `mu4e-drafts-folder' (therefore, these must be
2011-10-02 20:35:03 +02:00
set).
The message file name is a unique name determined by
2011-12-13 08:07:38 +01:00
`mu4e-msg-draft-file-name'.
2011-12-13 08:07:38 +01:00
The initial STR would be created from either `mu4e-msg-create-reply',
ar`mu4e-msg-create-forward' or `mu4e-msg-create-new'. The editing buffer is
using Gnus' `message-mode'."
2011-12-13 08:07:38 +01:00
(unless mu4e-maildir (error "mu4e-maildir not set"))
(unless mu4e-drafts-folder (error "mu4e-drafts-folder not set"))
2011-10-02 20:35:03 +02:00
(let ((draft
(if (member compose-type '(reply forward new))
2011-12-13 08:07:38 +01:00
(mu4e-msg-open-draft compose-type msg)
2011-10-02 20:35:03 +02:00
(if (eq compose-type 'edit)
(plist-get msg :path)
(error "unsupported compose-type %S" compose-type)))))
2011-10-02 20:35:03 +02:00
(unless (file-readable-p draft)
(error "Cannot read %s" path))
2011-11-09 07:55:39 +01:00
2011-10-02 20:35:03 +02:00
(find-file draft)
(message-mode)
2011-09-30 07:37:47 +02:00
(make-local-variable 'write-file-functions)
2011-10-02 20:35:03 +02:00
2011-09-30 07:37:47 +02:00
;; update the db when the file is saved...]
(add-to-list 'write-file-functions
2011-12-13 08:07:38 +01:00
(lambda() (mu4e-proc-add (buffer-file-name) mu4e-drafts-folder)))
2011-09-30 07:37:47 +02:00
2011-09-22 20:01:35 +02:00
;; hook our functions up with sending of the message
2011-12-13 08:07:38 +01:00
(add-hook 'message-sent-hook 'mu4e-msg-save-to-sent nil t)
(add-hook 'message-sent-hook 'mu4e-send-set-parent-flag nil t)
2011-09-30 07:37:47 +02:00
(let ((message-hidden-headers
`("^References:" "^Face:" "^X-Face:" "^X-Draft-From:"
"^User-agent:")))
(message-hide-headers))
2011-10-10 07:38:14 +02:00
(if (eq compose-type 'new)
(message-goto-to)
(message-goto-body))))
2011-12-13 08:07:38 +01:00
(defun mu4e-msg-save-to-sent ()
"Move the message in this buffer to the sent folder. This is
meant to be called from message mode's `message-sent-hook'."
2011-12-13 08:07:38 +01:00
(unless mu4e-sent-folder (error "mu4e-sent-folder not set"))
2011-11-13 11:44:54 +01:00
(save-excursion
(goto-char (point-min))
;; remove the --text follows this line-- separator
(if (search-forward-regexp (concat "^" mail-header-separator "\n"))
(replace-match "")
(error "cannot find mail-header-separator"))
(save-buffer)
2011-12-13 08:07:38 +01:00
(let ((docid (gethash (buffer-file-name) mu4e-path-docid-map)))
2011-09-19 23:20:59 +02:00
(unless docid (error "unknown message (%S)" (buffer-file-name)))
2011-09-18 22:57:46 +02:00
;; ok, all seems well, well move the message to the sent-folder
2011-12-13 08:07:38 +01:00
(mu4e-proc-move-msg docid mu4e-sent-folder "-T-D+S")
2011-09-19 23:20:59 +02:00
;; we can remove the value from the hash now, if we can establish there
;; are not other compose buffers using this very same docid...
2011-11-13 11:44:54 +01:00
2011-09-18 22:57:46 +02:00
;; mark the buffer as read-only, as its pointing at a non-existing file
2011-11-13 11:44:54 +01:00
;; now...
(kill-buffer-and-window)
(message "Message has been sent"))))
2011-12-13 08:07:38 +01:00
(defun mu4e-send-set-parent-flag ()
"Set the 'replied' flag on messages we replied to, and the
'passed' flag on message we have forwarded.
2011-10-02 20:35:03 +02:00
If a message has a '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.
2011-09-30 07:37:47 +02:00
2011-10-02 20:35:03 +02:00
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.
This is meant to be called from message mode's
`message-sent-hook'."
2011-10-02 20:35:03 +02:00
(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))
2011-10-04 07:12:47 +02:00
(setq forwarded-from (car-safe (last refs)))))))
;; remove the <>
(when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
2011-12-13 08:07:38 +01:00
(mu4e-proc-flag (match-string 1 in-reply-to) "+R"))
(when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
2011-12-13 08:07:38 +01:00
(mu4e-proc-flag (match-string 1 forwarded-from) "+P"))))
2011-12-13 08:07:38 +01:00
(provide 'mu4e-send)