* move emacs/ code to toys/mua; rename mu=>mua, many updates

This commit is contained in:
Dirk-Jan C. Binnema
2011-08-08 21:55:59 +03:00
parent 46c8e79fea
commit 74d00e26d4
11 changed files with 1306 additions and 1145 deletions

View File

@ -1,227 +0,0 @@
;;; mu-common.el -- part of mu
;;
;; 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:
;; mu message has functions to display a message
;;; Code:
(require 'cl)
(defvar mu-binary "/home/djcb/src/mu/src/mu" "name/path of the mu executable")
(defvar mu-muile-binary "/home/djcb/src/mu/toys/muile/muile"
"name/path of the muile executable")
(defvar mu-home nil
"path where mu stores it's data or nil for the defaults (typically, ~/.mu)")
(defvar mu-date-format-short "%x %X" "date format (in strftime(2)
notation) e.g. for mail headers")
(defvar mu-date-format-long "%c" "date format (in strftime(2)
notation) for the mail view and in replied/forwarded message quotations")
(defvar mu-folder-draft "/home/djcb/Maildir/")
(defface mu-date-face '((t (:foreground "#8c5353"))) "")
(defface mu-subject-face '((t (:foreground "#dfaf8f"))) "")
(defface mu-from-face '((t (:foreground "#7f9f7f"))) "")
(defface mu-to-face '((t (:foreground "#7f6655"))) "")
(defface mu-cc-face '((t (:foreground "#7f6666"))) "")
(defface mu-bcc-face '((t (:foreground "#7f6677"))) "")
(defface mu-body-face '((t (:foreground "#8cd0d3"))) "")
(defface mu-header-face '((t (:foreground "#7f9f7f"))) "")
(defface mu-size-face '((t (:foreground "#889f7f"))) "")
(defface mu-flag-face '((t (:foreground "#dc56cc"))) "")
(defface mu-path-face '((t (:foreground "#dc56cc"))) "")
(defface mu-unread-face '((t (:bold t))) "")
(defface mu-face '((t (:foreground "Gray" :italic t))) "")
(defvar mu-own-address "djcb" "regexp matching my own address")
;;; internal stuff
(defvar mu-parent-buffer nil "the parent buffer for a
buffer (buffer-local), i.e., the buffer we'll return to when this
buffer is killed")
(defun mu-binary-version ()
"get the version of the mu binary"
(let ((cmd (concat mu-binary
" --version | head -1 | sed 's/.*version //'")))
(substring (shell-command-to-string cmd) 0 -1)))
(defun mu-inspect (path)
"inspect message in a guile environment"
(let ((cmd (concat mu-muile-binary " --msg='" path "'")))
(ansi-term cmd "*mu-inspect")))
;; (defalias mu-find mu-headers-find)
;; (defalias mu-display mu-message-display)
(defun mu-str (str)
"return STR propertized as a mu string (for info, warnings
etc.)"
(propertize str 'face 'mu-face 'intangible t))
(setq mu-headers-fields
'(
(:date . 20)
(:flags . 4)
(:from-or-to . 22)
(:size . 8)
(:subject . 40)))
(setq mu-headers-date-format "%x %X")
(setq mu-header-fields
'( :from
:to
:subject
:date
:path))
(setq mu-own-address-regexp "djcb\\|diggler\\|bulkmeel")
(defvar mu-maildir nil "our maildir")
(defvar mu-folder nil "our list of special folders for jumping,
moving")
(defvar mu-maildir nil "location of your maildir, typically ~/Maildir")
(defvar mu-inbox-folder nil "location of your inbox folder")
(defvar mu-outbox-folder nil "location of your outbox folder")
(defvar mu-sent-folder nil "location of your sent folder")
(defvar mu-trash-folder nil "location of your trash-folder folder")
(setq
mu-maildir "/home/djcb/Maildir"
mu-inbox-folder "/inbox"
mu-outbox-folder "/outbox"
mu-sent-folder "/sent"
mu-trash-folder "/trash")
(defvar mu-quick-folders nil)
(setq mu-quick-folders
'("/archive" "/bulkarchive" "/todo"))
(defun mu-ask-maildir (prompt &optional fullpath)
"ask user with PROMPT for a maildir name, if fullpath is
non-nill, return the fulpath (ie, mu-maildir prepended to the
maildir"
(interactive)
(let*
((showfolders
(delete-dups
(append (list mu-inbox-folder mu-sent-folder) mu-quick-folders)))
(chosen (ido-completing-read prompt showfolders)))
(concat (if fullpath mu-maildir "") chosen)))
(defun mu-ask-key (prompt)
"Get a char from user, only accepting characters marked with [x] in prompt,
e.g. 'Reply to [a]ll or [s]ender only; returns the character chosen"
(let ((match 0) (kars '()))
(while match
(setq match (string-match "\\[\\(.\\)\\]" prompt match))
(when match
(setq kars (cons (match-string 1 prompt) kars))
(setq match (+ 1 match))))
(let ((kar)
(prompt (replace-regexp-in-string
"\\[\\(.\\)\\]"
(lambda(s)
(concat "[" (propertize (substring s 1 -1) 'face 'highlight) "]"))
prompt)))
(while (not kar)
(setq kar (read-char-exclusive prompt))
(unless (member (string kar) kars)
(setq kar nil)))
kar)))
;; both in mu-find.el and mu-view.el we have the path as a text property; in the
;; latter case we could have use a buffer-local variable, but using a
;; text-property makes this function work for both
(defun mu-get-path ()
"get the path (a string) of the message at point or nil if it
is not found; this works both for the header list and when
viewing a message"
(let ((path (get-text-property (point) 'path)))
(unless path (message "No message at point"))
path))
;; The message sexp looks something like:
;; (
;; :from (("Donald Duck" . "donald@example.com"))
;; :to (("Mickey Mouse" . "mickey@example.com"))
;; :subject "Wicked stuff"
;; :date (20023 26572 0)
;; :size 15165
;; :msgid "foobar32423847ef23@pluto.net"
;; :path "/home/mickey/Maildir/inbox/cur/1312254065_3.32282.pluto,4cd5bd4e9:2,"
;; :priority high
;; :flags (new unread)
;; :body-txt " <message body>"
;; )
(defun mu-get-message (path)
"use 'mu view --format=sexp' to get the message at PATH in the
form of an s-expression; parse this s-expression and return the
Lisp data as a plist. Returns nil in case of error"
(if (not (file-readable-p path))
(progn (message "Message is not readable") nil)
(let* ((cmd (concat mu-binary " view --format=sexp " path))
(str (shell-command-to-string cmd))
(msglst (read-from-string str)))
(if msglst
(car msglst)
(progn (message "Failed to parse message") nil)))))
(defun mu-quit-buffer ()
"kill this buffer, and switch to it's parentbuf if it is alive"
(interactive)
(let ((parentbuf mu-parent-buffer))
(kill-buffer)
(when (and parentbuf (buffer-live-p parentbuf))
(switch-to-buffer parentbuf))))
(defun mu-get-new-buffer (bufname)
"return a new buffer BUFNAME; if such already exists, kill the
old one first"
(when (get-buffer bufname)
(kill-buffer bufname))
(get-buffer-create bufname))
(defun mu-log (frm &rest args)
(with-current-buffer (get-buffer-create "*mu-log*")
(goto-char (point-max))
(insert (apply 'format
(concat
(format-time-string "%x %X " (current-time))
frm "\n") args))))
(provide 'mu-common)

View File

@ -1,464 +0,0 @@
;;; mu-headers.el -- use `mu' from emacs
;; 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:
;;; Code:
(require 'mu-common)
;;; mu-headers has functions for displaying/manipulating a list of headers (ie.,
;;; one line descriptions of an e-mail message), based on the output of 'mu
;;; find'.
;; data is stored like the following: for each header-line, we take the (point)
;; at beginning-of-line (bol) and use that as the key in the mu-headers-hash
;; hash, which does
;;
;; point-of-bol -> path
;;
;; then, marks are stored in a seperate hash 'mu-headers-marks-hash, using
;;
;; point-of-bol -> (src . target)
;;
;; and note both 'delete' (target=/dev/null), trash (target=trash-folder), and
;; move can be expressed by that
;;
;; after the marks have been 'executed', the lines will be marked a *invisible*
;; instead of deleting them; that way, the 'point-of-bol' stays valid.
(defvar mu-headers-hash nil "internal: buffer-local hash table
which maps bol->path")
(defvar mu-headers-marks-hash nil "internal: buffer-local hash table
which maps bol->(src . target) for marked lines")
(defun mu-headers-set-path (path)
"map the bol of the current header to a path"
(puthash (line-beginning-position 1) path mu-headers-hash))
(defun mu-headers-get-path ()
"get the path for the header at point"
(gethash (line-beginning-position 1) mu-headers-hash))
(defvar mu-headers-fields
'( (:date . 25)
(:from-or-to . 22)
(:subject . 40))
"a list of fields and their widths")
(defvar mu-headers-sort-field "date"
"shortcut of the field to sort on (see mu-headers (1))")
(defvar mu-headers-sort-descending nil
"whether to sort in descending order")
;; internal stuff
(defconst mu-headers-buffer-name " *mu-headers*" "name of the mu
results buffer; name should start with a space")
(defvar mu-headers-process nil "the possibly running find process")
(defconst mu-eom "\n;;eom\n" "marker for the end of message in
the mu find output")
(defvar mu-headers-expression nil
"search expression for the current find buffer")
(defvar mu-buf "" "internal: buffer for results data")
(defun mu-headers-process-filter (proc str)
"process-filter for the 'mu find --format=sexp output; it
accumulates the strings into valid sexps by checking of the
';;eom' end-of-msg marker, and then evaluating them"
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(save-excursion
(setq mu-buf (concat mu-buf str))
(let ((eom (string-match mu-eom mu-buf)))
(while (numberp eom)
(let* ((msg (car (read-from-string (substring mu-buf 0 eom))))
(inhibit-read-only t))
(goto-char (point-max))
(mu-headers-set-path (plist-get msg :path))
(save-match-data (insert (mu-headers-header msg) ?\n)))
(setq mu-buf (substring mu-buf (match-end 0)))
(setq eom (string-match mu-eom mu-buf))))))))
(defun mu-headers-process-sentinel (proc msg)
"Check the mu-headers process upon completion"
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(let ((status (process-status proc))
(exit-status (process-exit-status proc)))
(if (memq status '(exit signal))
(let ((inhibit-read-only t)
(text
(cond
((eq status 'signal)
"Search process killed (results incomplete)")
((eq status 'exit)
(cond
((= 0 exit-status) "End of search results")
((= 2 exit-status) "No matches found")
((= 4 exit-status) "Database problem; try running 'mu index'")
(t (format "Some error occured; mu-headers returned %d"
exit-status))))
(t "Unknown status")))) ;; shouldn't happen
(save-excursion
(goto-char (point-max))
(insert (mu-str text)))))))))
;; Note, the 'mu find --format=sexp' sexp is almost the same as the ones that
;; 'mu view --format=sexp' produces (see mu-get-message), with the difference
;; that former may give more than one result, and that mu-headers output comes
;; from the database rather than file, and does _not_ contain the message body
(defun mu-headers-search (expr)
"search in the mu database"
(interactive "s[mu] search for: ")
(let* ((buf (mu-get-new-buffer mu-headers-buffer-name))
(dummy-arg "--fields=\"dummy\"") ;; ignored
(proc (start-process mu-headers-buffer-name buf
mu-binary
"find"
(if mu-home
(concat "--muhome=" mu-home) dummy-arg)
(if mu-headers-sort-field
(concat "--sortfield=" mu-headers-sort-field) dummy-arg)
(if mu-headers-sort-descending "--descending" dummy-arg)
"--format=sexp"
"--quiet"
expr)))
(mu-log "search: '%s'" expr)
(switch-to-buffer buf)
(mu-headers-mode)
(setq
mu-buf "" ;; if the last query went wrong...
mu-headers-expression expr
mu-headers-process proc
mu-headers-hash (make-hash-table :size 256 :rehash-size 2)
mu-headers-marks-hash (make-hash-table :size 16 :rehash-size 2))
(set-process-filter proc 'mu-headers-process-filter)
(set-process-sentinel proc 'mu-headers-process-sentinel)))
(defun mu-headers-field-contact (lst width face)
"display a list of contacts, truncated for fitting in WIDTH"
(if lst
(let* ((len (length lst))
(str (if (= len 0) "<none>"
;; try name -> email -> ?
(or (car(car lst)) (cdr(car lst)) "?")))
(others (if (> len 1) (mu-str (format " [+%d]" (- len 1))) "")))
(truncate-string-to-width
(concat(propertize (truncate-string-to-width str
(- width (length others)) 0 ?\s "...") 'face face) others)
width 0 ?\s))
(make-string width ?\s)))
(defun mu-headers-field-from-or-to (fromlst tolst width from-face to-face)
"return a propertized string for FROM unless TO matches
mu-own-address, in which case it returns TO, prefixed with To:"
(if (and fromlst tolst)
(let ((fromaddr (cdr(car fromlst))))
(if (and fromaddr (string-match mu-own-address fromaddr))
(concat (mu-str "To ") (mu-headers-field-contact tolst (- width 3) to-face))
(mu-headers-field-contact fromlst width from-face)))
(make-string width ?\s)))
(defun mu-headers-field-size (size width face)
"return a string for SIZE of WIDTH with FACE"
(let* ((str
(cond
((>= size 1000000) (format "%2.1fM" (/ size 1000000.0)))
((and (>= size 1000) (< size 1000000)) (format "%2.1fK" (/ size 1000.0)))
((< size 1000) (format "%d" size)))))
(propertize (truncate-string-to-width str width 0 ?\s) 'face face)))
(defun mu-headers-field-str (str width face)
"print a STR, at WIDTH (truncate or ' '-pad) with FACE"
(let ((str (if str str "")))
(propertize (truncate-string-to-width str width 0 ?\s t) 'face face)))
(defun mu-headers-field-flags (flags width face)
(let ((str
(mapconcat
(lambda(flag)
(let ((flagname (symbol-name flag)))
(cond
((string= flagname "unread") "U")
((string= flagname "seen") "S")
((string= flagname "replied") "R")
((string= flagname "attach") "a")
((string= flagname "encrypted") "x")
((string= flagname "signed") "s")))) flags "")))
(propertize (truncate-string-to-width str width 0 ?\s) 'face face)))
(defun mu-headers-field (msg fieldinfo)
"determine a field based on FIELDINFO in the header for MSG"
(let* ((field (car fieldinfo))
(width (cdr fieldinfo))
(val (plist-get msg field)) ;; note: header-field maps msg-field in
(str (case field ;; most cases..
(:date (mu-headers-field-str (format-time-string mu-date-format-short
val) width 'mu-date-face))
(:from (mu-headers-field-contact val width 'mu-from-face))
(:to (mu-headers-field-contact val width 'mu-to-face))
(:cc (mu-headers-field-contact val width 'mu-cc-face))
(:bcc (mu-headers-field-contact val width 'mu-bcc-face))
(:flags (mu-headers-field-flags val width 'mu-flag-face))
(:size (mu-headers-field-size val width 'mu-size-face))
(:subject (mu-headers-field-str val width 'mu-subject-face))
(:from-or-to ;; this one is special
(mu-headers-field-from-or-to (plist-get msg :from)
(plist-get msg :to) width 'mu-from-face 'mu-to-face)))))
str))
(defun mu-headers-header (msg)
"convert a message s-expression into a header for display, and
set text property 'path"
(concat " "
(mapconcat
(lambda (fieldinfo)
(mu-headers-field msg fieldinfo)) mu-headers-fields " ")))
(defun mu-headers-mode ()
"major mode for displaying search results"
(interactive)
(kill-all-local-variables)
(use-local-map mu-headers-mode-map)
(make-local-variable 'mu-buf)
(make-local-variable 'mu-parent-buffer)
(make-local-variable 'mu-headers-expression)
(make-local-variable 'mu-headers-process)
(make-local-variable 'mu-headers-hash)
(make-local-variable 'mu-headers-marks-hash)
(setq
major-mode 'mu-headers-mode mode-name "*headers*"
mu-buf ""
truncate-lines t buffer-read-only t
overwrite-mode 'overwrite-mode-binary))
(defvar mu-headers-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "s" 'mu-headers-search)
(define-key map "q" 'mu-quit-buffer)
(define-key map "s" 'mu-headers-change-sort)
(define-key map "g" 'mu-headers-refresh)
;; navigation
(define-key map "n" 'mu-headers-next)
(define-key map "p" 'mu-headers-previous)
(define-key map "j" 'mu-headers-jump-to-maildir)
;; marking/unmarking/executing
(define-key map "m" 'mu-headers-mark-for-move)
(define-key map "d" 'mu-headers-mark-for-trash)
(define-key map "D" 'mu-headers-mark-for-deletion)
(define-key map "u" 'mu-headers-unmark)
(define-key map "U" 'mu-headers-unmark-all)
(define-key map "x" 'mu-headers-marked-execute)
;; message composition
(define-key map "r" 'mu-reply)
(define-key map "f" 'mu-forward)
(define-key map (kbd "RET") 'mu-headers-view)
map)
"Keymap for \"mu-headers\" buffers.")
(fset 'mu-headers-mode-map mu-headers-mode-map)
(defun mu-headers-view ()
"display the message at the current line"
(interactive)
(let ((path (mu-headers-get-path)))
(when path (mu-view path (current-buffer)))))
(defun mu-headers-next ()
"go to the next line; t if it worked, nil otherwise"
(interactive)
(if (or (/= 0 (forward-line 1)) (not (mu-headers-get-path)))
(progn (message "No message after this one") nil)
t))
(defun mu-headers-prev ()
"go to the next line; t if it worked, nil otherwise"
(interactive)
(if (/= 0 (forward-line -1))
(progn (message "No message before this one") nil)
t))
(defun mu-headers-jump-to-maildir ()
"show the messages in one of the standard folders"
(interactive)
(let ((fld (mu-ask-maildir "Jump to maildir: ")))
(mu-headers-search (concat "maildir:" fld))))
(defun mu-headers-refresh ()
"re-run the query for the current search expression, but only
if the search process is not already running"
(interactive)
(message "REFRESH %s" mu-headers-expression)
(if (and mu-headers-process (eq (process-status mu-headers-process) 'run))
(message "Can't refresh while running")
(when mu-headers-expression (mu-headers mu-headers-expression))))
;; create a new query based on the old one, but with a changed sort order
(defun mu-headers-change-sort-order (fieldchar)
"change the sortfield to FIELDCHAR"
(interactive"cField to sort by ('d', 's', etc.; see mu-headers(1)):\n")
(let
((field
(case fieldchar
(?b "bcc")
(?c "cc")
(?d "date")
(?f "from")
(?i "msgid")
(?m "maildir")
(?p "prio")
(?s "subject")
(?t "to")
(?z "size"))))
(if field
(setq mu-headers-sort-field field)
(message "Invalid sort-field; use one of bcdfimpstz (see mu-headers(1)"))
field))
(defun mu-headers-change-sort-direction (dirchar)
"change the sort direction, either [a]scending or [d]escending"
(interactive
"cSorting direction ([a]scending or [d]escending):")
(cond
(?d (setq mu-headers-sort-descending t) t)
(?a (setq mu-headers-sort-descending nil) t)
(t (message
"Invalid sort-direction; choose either [a]scending or [d]escending") nil)))
(defun mu-headers-change-sort ()
"change sort field and direction"
(interactive)
(and (call-interactively 'mu-headers-change-sort-order)
(call-interactively 'mu-headers-change-sort-direction)))
(defun mu-headers-add-marked (src &optional dst)
(let ((bol (line-beginning-position 1)))
(if (gethash bol mu-headers-marks-hash)
(progn (message "Message is already marked") nil)
(progn (puthash bol (cons src dst) mu-headers-marks-hash) t))))
(defun mu-headers-remove-marked ()
(let ((bol (line-beginning-position 1)))
(if (not (gethash bol mu-headers-marks-hash))
(progn (message "Message is not marked") nil)
(progn (remhash bol mu-headers-marks-hash) t))))
(defun mu-headers-set-marker (kar)
"set the marker at the beginning of this line"
(beginning-of-line 1)
(let ((inhibit-read-only t))
(delete-char 2)
(insert (if kar kar " ") " ")))
(defun mu-headers-mark (action)
"mark the current msg for something: move, delete, trash, unmark"
(let ((target) (src (mu-headers-get-path)))
(when src
(case action
(move
(when (mu-headers-add-marked src
(mu-ask-maildir "Target maildir: " t))
(mu-headers-set-marker ?m)))
(trash
(when (mu-headers-add-marked src
(concat mu-maildir mu-trash-folder))
(mu-headers-set-marker ?d)))
(delete
(when (mu-headers-add-marked src "/dev/null")
(mu-headers-set-marker ?D)))
(unmark
(when (mu-headers-remove-marked)
(mu-headers-set-marker nil)))
(unmark-all
(when (y-or-n-p (format "Sure you want to remove all (%d) marks? "
(hash-table-count mu-headers-marks-hash)))
(save-excursion
(maphash (lambda (k v) (goto-char k) (mu-headers-mark 'unmark))
mu-headers-marks-hash)))
(t (message "Unsupported mark type"))))
(move-beginning-of-line 2))))
(defun mu-headers-marks-execute ()
"execute the actions for all marked messages"
(interactive)
(let ((n-marked (hash-table-count mu-headers-marks-hash)))
(if (= 0 n-marked)
(message "No marked messages")
(when (y-or-n-p
(format "Execute actions for %d marked message(s)? " n-marked))
(save-excursion
(maphash
(lambda(bol v)
(let ((src (car v)) (target (cdr v)) (inhibit-read-only t))
(when (mu-message-move src target)
(goto-char bol)
(mu-headers-remove-marked)
(put-text-property (line-beginning-position 1)
(line-beginning-position 2)
'invisible t)))) ;; when it succeedes, hide msg..)
mu-headers-marks-hash))
(message "Done")
))))
(defun mu-headers-mark-for-move () (interactive) (mu-headers-mark 'move))
(defun mu-headers-mark-for-trash () (interactive) (mu-headers-mark 'trash))
(defun mu-headers-mark-for-delete () (interactive) (mu-headers-mark 'delete))
(defun mu-headers-mark-for-deletion () (interactive) (mu-headers-mark 'delete))
(defun mu-headers-unmark () (interactive) (mu-headers-mark 'unmark))
(defun mu-headers-unmark-all () (interactive) (mu-headers-mark 'unmark-all))
(defun mu-headers-reply ()
"Reply to the message at point"
(interactive)
(let ((path (mu-headers-get-path)))
(if path
(mu-message-reply path)
(message "No message at point"))))
(defun mu-headers-forward ()
"Reply to the message at point"
(interactive)
(let ((path (mu-headers-get-path)))
(if path
(mu-message-forward path)
(message "No message at point"))))
(provide 'mu-headers)

View File

@ -1,165 +0,0 @@
;;; mu-message.el -- use `mu' from emacs
;; 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:
;; mu-message contains code to generate a message for composing, replying or
;; forwarding
;;; Code:
(require 'mu-common)
(defvar mu-message-citation-prefix "> "
"string to prefix cited message parts with")
(defvar mu-message-reply-prefix "Re:"
"string to prefix the subject of replied messages with")
(defvar mu-message-forward-prefix "Fwd:"
"string to prefix the subject of forwarded messages with")
(defun mu-message-user-agent ()
(format "mu %s; emacs %s" (mu-binary-version) emacs-version))
(defun mu-message-attribution (msg)
"get an attribution line for a quoted message"
(format "On %s, %s wrote:\n"
(format-time-string mu-date-format-long (plist-get msg :date))
(cdr (car (plist-get msg :from)))))
(defun mu-message-cite (msg)
"cite an existing message"
(let ((body
(or (plist-get msg :body-txt)
(let ((html (plist-get msg :body-html)))
(when html
(with-temp-buffer (insert html) (html2text) (buffer-string))))
"")))
(replace-regexp-in-string "^" " > " body)))
(defun mu-message-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)
(string= email-to-remove (downcase (cdr name-email))))
lst))
(defun mu-message-recipients-to-string (lst)
"convert a recipient list (of form '( (\"A\"
. \"a@example.com\") (\"B\" . \"B@example.com\") into a string
useful for from/to headers"
(message "recips: %S" lst)
(mapconcat
(lambda (recip)
(let ((name (car recip)) (email (cdr recip)))
(format "%s <%s>" (or name "") email))) lst ", "))
(defun mu-message-hidden-header (hdr val)
"return user-invisible header to the message (HDR: VAL\n)"
(propertize (format "%s: %s\n" hdr val) 'invisible t))
(defun mu-message-reply (path)
"create a reply to the message at PATH. After creation, switch
to the message editor"
(let* ((cmd (concat mu-binary " view --format=sexp " path))
(str (shell-command-to-string cmd))
(msg (car (read-from-string str)))
(buf (get-buffer-create
(generate-new-buffer-name "*mu-draft*")))
(to-lst (mu-message-recipients-remove
(append (plist-get msg :from) (plist-get msg :to))
user-mail-address))
(cc-lst (mu-message-recipients-remove (plist-get msg :cc)
user-mail-address)))
(with-current-buffer buf
(insert
(format "From: %s <%s>\n" user-full-name user-mail-address)
(mu-message-hidden-header "User-agent" (mu-message-user-agent))
(if (boundp 'mail-reply-to) (insert (format "Reply-To: %s\n"
mail-reply-to)) "")
(format "To: %s\n" (if to-lst (mu-message-recipients-to-string to-lst) ""))
(if cc-lst
(format "Cc: %s\n" (mu-message-recipients-to-string cc-lst)))
"Subject: " mu-message-reply-prefix (plist-get msg :subject) "\n"
"--text follows this line--\n\n"
(mu-message-attribution msg)
(mu-message-cite msg)))
(switch-to-buffer buf)
(message-mode)
(message-goto-body)))
(defun mu-message-forward (path)
"create a forward to the message at PATH. After creation, switch
to the message editor"
(let* ((cmd (concat mu-binary " view --format=sexp " path))
(str (shell-command-to-string cmd))
(msg (car (read-from-string str)))
(buf (get-buffer-create
(generate-new-buffer-name "*mu-draft*"))))
(with-current-buffer buf
(insert
(format "From: %s <%s>\n" user-full-name user-mail-address)
(mu-message-hidden-header "User-agent" (mu-message-user-agent))
"To: \n"
"Subject: " mu-message-forward-prefix (plist-get msg :subject) "\n"
"--text follows this line--\n\n"
(mu-message-attribution msg)
(mu-message-cite msg)))
(switch-to-buffer buf)
(message-mode)
(message-goto-to)))
(defun mu-message-move (src targetdir)
"move message at PATH using 'mu mv'; if targetdir is
'/dev/null', move immediately. Return t if succeeded, nil
otherwise"
(let* ((cmd (concat
mu-binary " mv --printtarget "
(shell-quote-argument src) " "
(shell-quote-argument targetdir)))
(fulltarget (shell-command-to-string cmd)))
(mu-log cmd)
(mu-log
(if fulltarget (concat "Message has been moved to " fulltarget)
"Message moving failed"))
;; now, if saving worked, anynchronously try to update the database
(when fulltarget
(mu-log "Removing from database: %s" src)
(start-process " *mu-remove*" nil mu-binary "remove" src)
(if (string= targetdir "/dev/null")
t
(mu-log "Adding to database: %s" fulltarget)
(start-process " *mu-add*" nil mu-binary "add" fulltarget) t))))
;; note, we don't check the result of the db output
(provide 'mu-message)

View File

@ -1,221 +0,0 @@
;;; mu-view.el -- use `mu' from emacs
;; 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 theq
;; 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:
;; mu message has functions to display a single message
;;; Code:
(require 'mu-common)
(defvar mu-view-header-fields
'( :from
:to
:subject
:date
:attachments
:path)
"list of header fields to display in the message view")
(defconst mu-view-buffer-name " *mu-view*")
(defvar mu-view-headers-buffer nil "the headers buffer (if any)
from which this buffer was invoked (buffer local)")
(defun mu-view-header (field val val-face)
"get a header string (like 'Subject: foo')"
(when val
(concat (propertize field 'face 'mu-header-face) ": "
(propertize val 'face val-face) "\n")))
(defun mu-view-header-contact (field lst face)
(when lst
(let* ((header (concat (propertize field 'face 'mu-header-face) ": "))
(val (mapconcat (lambda(c)
(propertize (or (car c) (cdr c) "?") 'face face))
lst ",")))
(concat header val "\n"))))
(defun mu-view-header-contact (field lst face)
(when lst
(let* ((header (concat (propertize field 'face 'mu-header-face) ": "))
(val (mapconcat (lambda(c)
(propertize (or (car c) (cdr c) "?") 'face face))
lst ", ")))
(concat header val "\n"))))
(defun mu-view-header-attachments (field lst face)
(when lst
(let* ((header (concat (propertize field 'face 'mu-header-face) ": "))
(val (mapconcat
(lambda(att)
(let ((idx (nth 0 att)) (fname (nth 1 att)) (ctype (nth 2 att)))
(propertize fname 'face face)))
lst ", ")))
(concat header val "\n"))))
(defun mu-view-body (msg face)
"view the body; try text first, if that does not work, try html"
(cond
((plist-get msg :body-txt) (propertize (plist-get msg :body-txt) 'face face))
((plist-get msg :body-html)
(propertize
(with-temp-buffer
(insert (plist-get msg :body-html))
(html2text)
(buffer-string)) 'face face))
(t "")))
(defun mu-view-message (path)
"display the email message at PATH"
(let ((msg (mu-get-message path)))
(when msg
(concat
(mapconcat
(lambda (field)
(case field
(:from (mu-view-header-contact "From"
(plist-get msg :from) 'mu-from-face))
(:to
(mu-view-header-contact "To" (plist-get msg :to) 'mu-to-face))
(:cc
(mu-view-header-contact "Cc" (plist-get msg :cc) 'mu-to-face))
(:bcc
(mu-view-header-contact "Bcc" (plist-get msg :bcc) 'mu-to-face))
(:subject
(mu-view-header "Subject" (plist-get msg :subject) 'mu-subject-face))
(:path
(mu-view-header "Path" (plist-get msg :path) 'mu-path-face))
(:date
(mu-view-header "Date"
(format-time-string mu-date-format-long
(plist-get msg :date)) 'mu-date-face))
(:attachments
(mu-view-header-attachments "Attachments" (plist-get msg :attachments)
'mu-path-face)
)))
mu-view-header-fields "")
"\n"
(mu-view-body msg 'mu-body-face)
))))
;; note: mu-view sets path as a text-property ('path) for the whole buffer, just
;; like mu-headers does it per-header
(defun mu-view (path parentbuf)
"display message at PATH in a new buffer; note that the action
of viewing a message may cause it to be moved/renamed; this
function returns the resulting name. PARENTBUF refers to the
buffer who invoked this view; this allows us to return there when
we quit from this view. Also, if PARENTBUF is a find buffer (ie.,
has mu-headers-mode as its major mode), this allows various
commands (navigation, marking etc.) to be applied to this
buffer."
(let ((str (mu-view-message path))
(buf (mu-get-new-buffer mu-view-buffer-name)))
(when str
(switch-to-buffer buf)
(insert str))
(mu-view-mode)
(setq ;; these are buffer-local
mu-parent-buffer parentbuf
mu-view-headers-buffer parentbuf
mu-path path)
(goto-char (point-min))))
(defvar mu-view-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" 'mu-quit-buffer)
(define-key map "s" 'mu-headers)
(define-key map "f" 'mu-forward)
(define-key map "r" 'mu-reply)
;; navigation between messages
(define-key map "n" 'mu-view-next)
(define-key map "p" 'mu-view-prev)
;; marking/unmarking
(define-key map "d" '(lambda (mu-view-mark 'trash)))
(define-key map "D" '(lambda (mu-view-mark 'delete)))
(define-key map "m" '(lambda (mu-view-mark 'move)))
(define-key map "u" '(lambda (mu-view-mark 'unmark)))
(define-key map "x" 'mu-view-marked-execute)
map)
"Keymap for \"mu-view\" buffers.")
(fset 'mu-view-mode-map mu-view-mode-map)
(defun mu-view-mode ()
"major mode for viewing an e-mail message"
(interactive)
(kill-all-local-variables)
(use-local-map mu-view-mode-map)
(make-local-variable 'mu-parent-buffer)
(make-local-variable 'mu-headers-buffer)
(make-local-variable 'mu-path)
(setq major-mode 'mu-view-mode mode-name "*mu-view*")
(setq truncate-lines t buffer-read-only t))
(defmacro with-current-headers-buffer (&rest body)
"Execute the forms in BODY with BUFFER-OR-NAME temporarily current.
BUFFER-OR-NAME must be a buffer or the name of an existing buffer.
The value returned is the value of the last form in BODY. See
also `with-temp-buffer'."
(declare (indent 1) (debug t))
`(if (and mu-view-headers-buffer (buffer-live-p mu-view-headers-buffer))
(save-current-buffer
(set-buffer mu-view-headers-buffer)
,@body)
(message "No headers-buffer connected")))
(defun mu-view-next ()
"move to the next message"
(interactive)
(with-current-headers-buffer
(when (mu-headers-next)
(mu-view (mu-headers-get-path) (current-buffer)))))
(defun mu-view-prev ()
"move to the previous message"
(interactive)
(with-current-headers-buffer
(when (mu-headers-prev)
(mu-view (mu-headers-get-path) (current-buffer)))))
(defun mu-view-mark (mark)
"mark for MARK"
(interactive)
(with-current-headers-buffer (mu-headers-mark mark)))
;; we don't allow executing marks from the view buffer, to protect user from
;; accidentally deleting stuff...
(defun mu-view-marked-execute ()
"give user a warning"
(interactive)
(message "Please go back to the headers list to execute your marks"))
(provide 'mu-view)

View File

@ -1,68 +0,0 @@
;;; mu.el -- use `mu' from emacs
;; 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:
;;; Code:
(require 'mu-view)
(require 'mu-headers)
(require 'mu-message)
(define-key mu-headers-mode-map "q" 'mu-quit-buffer)
(define-key mu-headers-mode-map "f" 'mu-headers)
(define-key mu-headers-mode-map (kbd "<up>") 'mu-headers-prev)
(define-key mu-headers-mode-map (kbd "<down>") 'mu-headers-next)
(define-key mu-headers-mode-map (kbd "RET") 'mu-headers-view)
(define-key mu-headers-mode-map "n" 'mu-headers-next)
(define-key mu-headers-mode-map "p" 'mu-headers-prev)
(define-key mu-headers-mode-map "o" 'mu-headers-change-sort)
(define-key mu-headers-mode-map "g" 'mu-headers-refresh)
(define-key mu-headers-mode-map "m" 'mu-headers-mark-for-move)
(define-key mu-headers-mode-map "d" 'mu-headers-mark-for-trash)
(define-key mu-headers-mode-map "D" 'mu-headers-mark-for-deletion)
(define-key mu-headers-mode-map "u" 'mu-headers-unmark)
(define-key mu-headers-mode-map "U" 'mu-headers-unmark-all)
(define-key mu-headers-mode-map "r" 'mu-headers-reply)
(define-key mu-headers-mode-map "f" 'mu-headers-forward)
(define-key mu-headers-mode-map "x" 'mu-headers-marks-execute)
(define-key mu-view-mode-map "q" 'mu-quit-buffer)
(define-key mu-view-mode-map "f" 'mu-view-find)
(define-key mu-view-mode-map "n" 'mu-view-next)
(define-key mu-view-mode-map "p" 'mu-view-prev)
(define-key mu-view-mode-map "r" 'mu-reply)
(define-key mu-view-mode-map "f" 'mu-forward)
(define-key mu-view-mode-map "x" 'mu-execute)
(define-key mu-view-mode-map "m" 'mu-view-mark-for-move)
(define-key mu-view-mode-map "d" 'mu-view-mark-for-trash)
(define-key mu-view-mode-map "D" 'mu-view-mark-for-deletion)
(define-key mu-view-mode-map "u" 'mu-view-unmark)
(define-key mu-view-mode-map "U" 'mu-view-unmark-all)
(define-key mu-view-mode-map "r" 'mu-view-reply)
(define-key mu-view-mode-map "f" 'mu-view-forward)
(define-key mu-view-mode-map "x" 'mu-view-marked-execute)
(provide 'mu)

28
toys/mua/TODO Normal file
View File

@ -0,0 +1,28 @@
* TODO
[ ] message un-new in find/view
[ ] set 'Replied' flag on source when message is replied
[ ] update database after changes (CHECK)
[ ] save message to draft, sent items
[ ] attachment handling (open, play) in view
[ ] abstract away mu/binary, shell-command-to-string
[ ] make flag handling a bit more lispy
[ ] threads support
[ ] expandable recipients list in view
[ ] additive font props in mu find
[ ] fix headers/view interaction
[ ] region commands
[ ] menu
[ ] mua-dashboard
# Local Variables:
# mode: org; org-startup-folded: nil
# End:

94
toys/mua/mua-common.el Normal file
View File

@ -0,0 +1,94 @@
;;; mua-common.el -- part of mua, 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:
;; mua-common contains common utility functions for mua
;;; Code:
(eval-when-compile (require 'cl))
(defconst mua/log-buffer-name "*mua-log*" "name of the logging buffer")
(defun mua/warn (frm &rest args)
"warn user in echo-area, return nil"
(let ((str (apply 'format frm args)))
(message str)
nil))
(defun mua/log (frm &rest args)
"write something in the *mua-log* buffer - mainly useful for debugging"
(with-current-buffer (get-buffer-create mua/log-buffer-name)
(goto-char (point-max))
(insert (apply 'format (concat (format-time-string "%x %X " (current-time))
frm "\n") args))))
(defun mua/warn-and-log (frm &rest args)
"log and warn (ie., mua/warn + mua/log); return nil"
(apply 'mua/log frm args)
(apply 'mua/warn frm args)
nil)
(defun mua/new-buffer (bufname)
"return a new buffer BUFNAME; if such already exists, kill the
old one first"
(when (get-buffer bufname)
(kill-buffer bufname))
(get-buffer-create bufname))
(defun mua/message (frm &rest args)
"print a mua message at point"
(let ((str (apply 'format frm args)) (inhibit-read-only t))
(insert (propertize str 'face 'italic))))
(defun mua/quit-buffer ()
"kill this buffer, and switch to it's parentbuf if it is alive"
(interactive)
(let ((parentbuf mua/parent-buffer))
(kill-buffer)
(when (and parentbuf (buffer-live-p parentbuf))
(switch-to-buffer parentbuf))))
(defun mua/ask-maildir (prompt &optional fullpath)
"ask user with PROMPT for a maildir name, if fullpath is
non-nill, return the fulpath (ie, mu-maildir prepended to the
maildir"
(interactive)
(let* ((showfolders
(delete-dups
(append (list mua/inbox-folder mua/sent-folder)
mua/working-folders)))
(chosen (ido-completing-read prompt showfolders)))
(concat (if fullpath mua/maildir "") chosen)))
(defun mua/mu-binary-version ()
"Get the version of the mu binary."
(let ((cmd (concat mua/mu-binary
" --version | head -1 | sed 's/.*version //'")))
(substring (shell-command-to-string cmd) 0 -1)))
(provide 'mua-common)

480
toys/mua/mua-hdrs.el Normal file
View File

@ -0,0 +1,480 @@
;;; mua-hdrs.el -- part of mua, 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:
;; mu
;;; Code:
(eval-when-compile (require 'cl))
(require 'mua-common)
(require 'mua-msg)
;; note: these next two are *not* buffer-local, so they persist during a session
(defvar mua/hdrs-sortfield nil "field to sort headers by")
(defvar mua/hdrs-sort-descending nil "whether to sort in descending order")
(defvar mua/header-fields
'( (:date . 25)
(:from-or-to . 22)
(:subject . 40))
"a list of fields and their widths")
;; internal stuff
(defvar mua/buf "" "buffer for results data")
(defvar mua/last-expression "the last search expression")
(defvar mua/hdrs-process "the mu-find process")
(defvar mua/hdrs-hash nil "the bol->path hash")
(defvar mua/hdrs-marks-hash nil "the hash for marked messages")
(defconst mua/eom "\n;;eom\n" "marker for the end of message in
the mu find output")
(defconst mua/hdrs-buffer-name "*mua-headers*"
"name of the mua headers buffer")
(defun mua/hdrs-proc-filter (proc str)
"process-filter for the 'mu find --format=sexp output; it
accumulates the strings into valid sexps by checking of the
';;eom' end-of-msg marker, and then evaluating them"
(let ((procbuf (process-buffer proc)))
(when (buffer-live-p procbuf)
(with-current-buffer procbuf
(save-excursion
(setq mua/buf (concat mua/buf str))
(let ((eom (string-match mua/eom mua/buf)))
(while (numberp eom)
(let* ((msg (mua/msg-from-string(substring mua/buf 0 eom))))
(save-match-data (mua/hdrs-append-message msg))
(setq mua/buf (substring mua/buf (match-end 0)))
(setq eom (string-match mua/eom mua/buf))))))))))
(defun mua/hdrs-proc-sentinel (proc msg)
"Check the process upon completion"
(let ((procbuf (process-buffer proc))
(status (process-status proc))
(exit-status (process-exit-status proc)))
(when (and (buffer-live-p procbuf) (memq status '(exit signal)))
(let ((msg
(case status
('signal "Search process killed (results incomplete)")
('exit
(case exit-status
(0 "End of search results")
(1 "mu find error")
(2 "No matches found")
(4 "Database problem; try running 'mu index'")
(t (format "Some error occured; mu find returned %d"
exit-status)))))))
(with-current-buffer procbuf
(save-excursion
(goto-char (point-max))
(mua/message msg)))
(unless (= exit-status 0)
(mua/log "mu find exit with %d" exit-status))))))
(defun mua/hdrs-search-execute (expr buf)
"search in the mu database; output the results in buffer BUF"
(let ((args `("find" "--format=sexp" ,expr)))
(when mua/mu-home
(add-to-list args (concat "--muhome=" mua/mu-home)))
(when mua/hdrs-sortfield
(add-to-list args (concat "--sortfield=" mua/hdrs-sortfield)))
(when mua/hdrs-sort-descending
(add-to-list args "--descending"))
(mua/log "Searching for %s with %S" expr args)
;; now, do it!
(let ((proc (apply 'start-process "*mua-headers*" buf mua/mu-binary args)))
(setq
mua/buf ""
mua/hdrs-process proc)
(set-process-filter proc 'mua/hdrs-proc-filter)
(set-process-sentinel proc 'mua/hdrs-proc-sentinel))))
;; Note, the 'mu find --format=sexp' sexp is almost the same as the ones that
;; 'mu view --format=sexp' produces (see mu-get-message), with the difference
;; that former may give more than one result, and that mu-headers output comes
;; from the database rather than file, and does _not_ contain the message body
(defun mua/hdrs-search (expr)
"search in the mu database"
(interactive "s[mu] search for: ")
(setq debug-on-error t)
;; kill running process if needed
(when (and mua/hdrs-process
(eq (process-status mua/hdrs-process) 'run))
(kill-process mua/hdrs-process))
(let ((buf (mua/new-buffer mua/hdrs-buffer-name)))
(switch-to-buffer buf)
(mua/hdrs-mode)
(setq
mua/last-expression expr
mua/hdrs-hash (make-hash-table :size 256 :rehash-size 2)
mua/hdrs-marks-hash (make-hash-table :size 16 :rehash-size 2))
(mua/log "searching for %S" expr)
(mua/hdrs-search-execute expr buf)))
(defun mua/hdrs-mode ()
"major mode for displaying mua search results"
(interactive)
(kill-all-local-variables)
(use-local-map mua/hdrs-mode-map)
(make-local-variable 'mua/buf)
(make-local-variable 'mua/last-expression)
(make-local-variable 'mua/hdrs-process)
(make-local-variable 'mua/hdrs-hash)
(make-local-variable 'mua/hdrs-marks-hash)
(setq
major-mode 'mu-headers-mode mode-name "*headers*"
truncate-lines t buffer-read-only t
overwrite-mode 'overwrite-mode-binary))
(defun mua/hdrs-line (msg)
"return line describing a message (ie., a header line)"
(let
((hdr
(mapconcat
(lambda(fieldpair)
(let ((field (car fieldpair)) (width (cdr fieldpair)))
(case field
(:subject (mua/hdrs-header msg :subject width))
(:to (mua/hdrs-contact msg field width))
(:from (mua/hdrs-contact msg field width))
;;(:from-or-to (mua/msg-header-header-from-or-to msg width 'mua/header-face))
(:cc (mua/hdrs-contact msg field width))
(:bcc (mua/hdrs-contact msg field width))
(:date (mua/hdrs-date msg width))
(:flags (mua/hdrs-flags msg width))
(:size (mua/hdrs-size msg width))
(t (error "Unsupported field: %S" field))
)))
mua/header-fields " ")))
hdr))
;;
;; Note: we maintain a hash table to remember what message-path corresponds to a
;; certain line in the buffer. (mua/hdrs-set-path, mua/hdrs-get-path)
;;
;; data is stored like the following: for each header-line, we
;; take the (point) at beginning-of-line (bol) and use that as the key in the
;; mu-headers-hash hash, which does
;;
;; point-of-bol -> path
;;
(defun mua/hdrs-set-path (path)
"map the bol of the current header to a path"
(puthash (line-beginning-position 1) path mua/hdrs-hash))
(defun mua/hdrs-get-path ()
"get the path for the header at point"
(gethash (line-beginning-position 1) mua/hdrs-hash))
(defun mua/hdrs-append-message (msg)
"append a message line to the buffer and register the message"
(let ((line (mua/hdrs-line msg)) (inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(mua/hdrs-set-path (mua/msg-field msg :path))
(insert " " line "\n"))))
;; Now follow a bunch of function to turn some message field in a
;; string for display
(defun mua/hdrs-header (msg field width)
"get a string at WIDTH (truncate or ' '-pad) for display as a
header"
(let* ((str (mua/msg-field msg field)) (str (if str str "")))
(propertize (truncate-string-to-width str width 0 ?\s t)
'face 'mua/header-face)))
(defun mua/hdrs-contact (msg field width)
"get display string for a list of contacts in a header, truncated for
fitting in WIDTH"
(unless (member field '(:to :from :bcc :cc))
(error "Illegal type for contact"))
(let* ((lst (mua/msg-field msg field))
(str (mapconcat
(lambda (ctc)
(let ((name (car ctc)) (email (cdr ctc)))
(or name email "?"))) lst ",")))
(propertize (truncate-string-to-width str width 0 ?\s t)
'face 'mua/contacts-face)))
(defun mua/hdrs-size (msg width)
"return a string for size of MSG of WIDTH"
(let* ((size (mua/msg-field msg :size))
((str
(cond
((>= size 1000000) (format "%2.1fM" (/ size 1000000.0)))
((and (>= size 1000) (< size 1000000)) (format "%2.1fK" (/ size 1000.0)))
((< size 1000) (format "%d" size)))))
(propertize (truncate-string-to-width str width 0 ?\s)
'face 'mua/header-face))))
(defun mua/hdrs-date (msg width)
"return a string for the date of MSG of WIDTH"
(let* ((date (mua/msg-field msg :date)))
(if date
(propertize (truncate-string-to-width (format-time-string "%x %X" date)
width 0 ?\s) 'face 'mua/date-face))))
(defun mua/hdrs-flags (msg width)
(let* ((flags (mua/msg-field msg :flags))
(flagstr
(mapconcat
(lambda(flag)
(case flag
('unread "U")
('seen "S")
('replied "R")
('attach "a")
('encrypted "x")
('signed "s"))) flags "")))
(propertize (truncate-string-to-width flagstr width 0 ?\s)
'face 'mua/header-face)))
;; some keybinding / functions for basic navigation
(defvar mua/hdrs-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "s" 'mua/hdrs-search)
(define-key map "q" 'mua/quit-buffer)
(define-key map "o" 'mua/hdrs-change-sort)
(define-key map "g" 'mua/hdrs-refresh)
;; navigation
(define-key map "n" 'mua/hdrs-next)
(define-key map "p" 'mua/hdrs-prev)
(define-key map "j" 'mua/hdrs-jump-to-maildir)
;; marking/unmarking/executing
(define-key map "m" (lambda()(interactive)(mua/hdrs-mark 'move)))
(define-key map "d" (lambda()(interactive)(mua/hdrs-mark 'trash)))
(define-key map "D" (lambda()(interactive)(mua/hdrs-mark 'delete)))
(define-key map "u" (lambda()(interactive)(mua/hdrs-mark 'unmark)))
(define-key map "U" (lambda()(interactive)(mua/hdrs-mark 'unmark-all)))
(define-key map "x" 'mua/hdrs-marks-execute)
;; message composition
(define-key map "r" 'mua/hdrs-reply)
(define-key map "f" 'mua/hdrs-forward)
(define-key map "c" 'mua/hdrs-compose)
(define-key map (kbd "RET") 'mua/hdrs-view)
map)
"Keymap for *mua-headers* buffers.")
(fset 'mua/hdrs-mode-map mua/hdrs-mode-map)
(defun mua/hdrs-next ()
"go to the next line; t if it worked, nil otherwise"
(interactive) ;; TODO: check if next line has path, if not, don't go there
(if (or (/= 0 (forward-line 1)) (not (mua/hdrs-get-path)))
(mua/warn "No message after this one")
(progn
(mua/log "At: %d [%S]" (point) (mua/hdrs-get-path)) t)))
(defun mua/hdrs-prev ()
"go to the previous line; t if it worked, nil otherwise"
(interactive)
(if (or (/= 0 (forward-line -1)) (not (mua/hdrs-get-path)))
(mua/warn "No message before this one")
(progn
(mua/log "At: %d [%S]" (point) (mua/hdrs-get-path)) t)))
(defun mua/hdrs-view ()
(interactive)
(let ((path (mua/hdrs-get-path)))
(if path
(mua/view path (current-buffer))
(mua/warn "No message at point"))))
(defun mua/hdrs-jump-to-maildir ()
"Show the messages in one of the standard folders."
(interactive)
(let ((fld (mua/ask-maildir "Jump to maildir: ")))
(mua/hdrs-search (concat "maildir:" fld))))
(defun mua/hdrs-refresh ()
"Re-run the query for the current search expression, but only
if the search process is not already running"
(interactive)
(when mua/last-expression (mua/hdrs-search mua/last-expression)))
;;; functions for sorting
(defun mua/hdrs-change-sort-order (fieldchar)
"Change the sortfield to FIELDCHAR."
(interactive "cField to sort by ('d', 's', etc.; see mu-headers(1)):\n")
(let ((field
(case fieldchar
(?b "bcc")
(?c "cc")
(?d "date")
(?f "from")
(?i "msgid")
(?m "maildir")
(?p "prio")
(?s "subject")
(?t "to")
(?z "size"))))
(if field
(setq mua/hdrs-sortfield field)
(mua/warn "Invalid sort-field; use one of bcdfimpstz (see mu-headers(1)"))
field))
(defun mua/hdrs-change-sort-direction (dirchar)
"Change the sort direction, either [a]scending or [d]escending."
(interactive)
(setq mua/hdrs-sort-descending
(y-or-n-p "Set sorting direction to descending(y) or ascending(n)")))
(defun mua/hdrs-change-sort ()
"Change thee sort field and direction."
(interactive)
(and (call-interactively 'mua/hdrs-change-sort-order)
(call-interactively 'mua/hdrs-change-sort-direction)))
;;; functions for marking
(defun mua/hdrs-add-marked (src &optional dst)
"Add the message at point to the markings hash"
(let ((bol (line-beginning-position 1)))
(if (gethash bol mua/hdrs-marks-hash)
(mua/warn "Message is already marked")
(progn (puthash bol (cons src dst) mua/hdrs-marks-hash) t))))
(defun mua/hdrs-remove-marked ()
"Remove the message at point from the markings hash"
(let ((bol (line-beginning-position 1)))
(if (not (gethash bol mua/hdrs-marks-hash))
(mua/warn "Message is not marked")
(progn (remhash bol mua/hdrs-marks-hash) t))))
(defun mua/hdrs-set-marker (kar)
"Set the marker at the beginning of this line."
(beginning-of-line 1)
(let ((inhibit-read-only t))
(delete-char 2)
(insert (if kar (format "%c " kar) " "))))
(defun mua/hdrs-mark (action)
"Mark the message at point with one of the symbols: move,
delete, trash, unmark, unmark-all; the latter two are
pseudo-markings."
(let ((target) (src (mua/hdrs-get-path)))
(when src
(case action
(move
(when (mua/hdrs-add-marked src
(mua/ask-maildir "Target maildir: " t)) ;; t->return fullpath
(mua/hdrs-set-marker ?m)))
(trash
(when (mua/hdrs-add-marked src
(concat mua/maildir mua/trash-folder))
(mua/hdrs-set-marker ?d)))
(delete
(when (mua/hdrs-add-marked src "/dev/null")
(mua/hdrs-set-marker ?D)))
(unmark
(when (mua/hdrs-remove-marked)
(mua/hdrs-set-marker nil)))
(unmark-all
(when (y-or-n-p (format "Sure you want to remove all (%d) marks? "
(hash-table-count mua/hdrs-marks-hash)))
(save-excursion
(maphash (lambda (k v) (goto-char k) (mua/hdrs-mark 'unmark))
mua/hdrs-marks-hash))))
(t (error "Unsupported mark type")))
(move-beginning-of-line 2))))
(defun mua/hdrs-marks-execute ()
"execute the actions for all marked messages"
(interactive)
(let ((n-marked (hash-table-count mua/hdrs-marks-hash)))
(if (= 0 n-marked)
(mua/warn "No marked messages")
(when (y-or-n-p
(format "Execute actions for %d marked message(s)? " n-marked))
(save-excursion
(maphash
(lambda(bol v)
(let ((src (car v)) (target (cdr v)) (inhibit-read-only t))
(when (mua/msg-move src target)
(goto-char bol)
(mua/hdrs-remove-marked)
(put-text-property (line-beginning-position 1)
(line-beginning-position 2)
'invisible t)))) ;; when it succeedes, hide msg..)
mua/hdrs-marks-hash))))))
;; functions for creating new message -- reply, forward, and new
(defun mua/hdrs-reply ()
"Reply to message at point."
(interactive)
(let* ((path (mua/hdrs-get-path))
(msg (when path (mua/msg-from-path path))))
(if msg
(mua/msg-compose (mua/msg-create-reply msg
(yes-or-no-p "Reply to all? ")))
(mua/warn "No message at point"))))
(defun mua/hdrs-forward ()
"Forward the message at point."
(interactive)
(let* ((path (mua/hdrs-get-path))
(msg (when path (mua/msg-from-path path))))
(if msg
(mua/msg-compose (mua/msg-create-forward msg))
(mua/warn "No message at point"))))
(defun mua/hdrs-compose ()
"Create a new messge."
(interactive)
(mua/msg-compose (mua/msg-create-new)))
(provide 'mua-hdrs)

483
toys/mua/mua-msg.el Normal file
View File

@ -0,0 +1,483 @@
;;; mua-msg.el -- part of mua, 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:
;; mua
;;; Code:
(eval-when-compile (require 'cl))
;; we use some stuff from gnus...
(require 'message)
(require 'mail-parse)
(require 'html2text)
(require 'mua-common)
(defun mua/msg-from-string (str)
"Get the plist describing an email message, from a string
contain a message sexp; a message sexp looks something like: The
message sexp looks something like:
\(
:from ((\"Donald Duck\" . \"donald@example.com\"))
:to ((\"Mickey Mouse\" . \"mickey@example.com\"))
:subject \"Wicked stuff\"
:date (20023 26572 0)
:size 15165
:references (\"200208121222.g7CCMdb80690@msg.id\")
:in-reply-to \"200208121222.g7CCMdb80690@msg.id\"
:message-id \"foobar32423847ef23@pluto.net\"
:path \"/home/mickey/Maildir/inbox/cur/1312254065_3.32282.pluto,4cd5bd4e9:2,\"
:priority high
:flags (new unread)
:attachments ((2 \"hello.jpg\" \"image/jpeg\") (3 \"laah.mp3\" \"audio/mp3\"))
:body-txt \" <message body>\"
\)
other fields are :cc, :bcc, :body-html
When the s-expression comes from the database ('mu find'), the
fields :attachments, :body-txt, :body-html, :references, :in-reply-to
are missing (because that information is not stored in the
database -- at least not in a usable way."
(condition-case nil
(car (read-from-string str));; read-from-string returns a cons
(error "Failed to parse message")))
(defun mua/msg-from-path (path)
"Get the an s-expression (plist) describing the e-mail message
at path, or nil if it failed. This functions uses 'mu view' to
obtain the s-expression. For the format, see `mua/msg-from-string'"
(if (not (file-readable-p path))
(progn
(mua/warn "Message is not readable")
(mua/log "not readable: %s" path))
(condition-case nil
(progn (let* ((cmd (concat mua/mu-binary " view --format=sexp " path))
(str (shell-command-to-string cmd)))
(mua/msg-from-string str)))
(error "Failed to read/parse message %s" path))))
(defun mua/msg-body-txt-or-html (msg)
"Get :body-txt, or if not available, :body-html converted to
text, using `html2text'."
(let ((body (mua/msg-field msg :body-txt)))
(unless body
(setq body (mua/msg-field msg :body-html))
(when body
(setq body (with-temp-buffer
(insert body)
(html2text)
(buffer-string)))))
body))
(defun mua/msg-field (msg field)
"Get a field from this message, or nil. The fields are the
fields of the message, which are the various items of the plist
as described in `mua/msg-from-string'
There is also the special field :body (which is either :body-txt,
or if not available, :body-html converted to text)."
(case field
(:body (mua/msg-body-txt-or-html msg))
(t (plist-get msg field))))
;; TODO: add better error-reporting to mua/msg-move, and make flag handling a
;; bit more lispy
(defun mua/msg-move (src targetdir &optional flags)
"Move message at SRC to TARGETDIR using 'mu mv'; SRC must be
the full, absolute path to a message file, while TARGETDIR must
be a maildir - that is, the part _without_ cur/ or new/. 'mu mv'
will calculate the target directory and the exact file name.
Optionally, you can specify the FLAGS for the new file; this must
be a string consisting of one or more of DFNPRST, mean
resp. Deleted, Flagged, New, Passed Replied, Seen and Trash, as
defined in [1].
If TARGETDIR is '/dev/null', remove SRC. After the file system
move, the database will be updated as well, using the 'mu add'
and 'mu remove' commands.
Function returns the target filename if the move succeeds, or
/dev/null if TARGETDIR was /dev/null; in other cases, it returns
`nil'.
\[1\] http://cr.yp.to/proto/maildir.html."
;; require the flags to be kosher
(when (and flags (let ((case-fold-search nil))
(string-match "[^DFNPRST]" flags))) (error Illegal flags))
(let* ((cmd (concat
mua/mu-binary " mv --printtarget "
(when flags (concat "--flags=" flags " "))
(shell-quote-argument src) " "
(shell-quote-argument targetdir)))
(fulltarget (shell-command-to-string cmd)))
(mua/log cmd)
(mua/log
(if fulltarget (concat "Message has been moved to " fulltarget)
"Message moving failed"))
;; now, if saving worked, anynchronously try to update the database
(when fulltarget ;; note, we don't check the result of the db output
(mua/log "Removing from database: %s" src)
(start-process " *mu-remove*" nil mua/mu-binary "remove" src)
(unless (string= targetdir "/dev/null")
(mua/log "Adding to database: %s" fulltarget)
(start-process " *mu-add*" nil mua/mu-binary "add" fulltarget) t)
)
fulltarget))
;; functions for composing new messages (forward, reply and new)
(defvar mua/msg-citation-prefix "> "
"String to prefix cited message parts with.")
(defvar mua/msg-reply-prefix "Re: "
"String to prefix the subject of replied messages with.")
(defvar mua/msg-forward-prefix "Fwd: "
"String to prefix the subject of forwarded messages with.")
(defconst mua/msg-draft-name "*mua-draft*"
"Name for draft messages.")
(defun mua/msg-user-agent ()
"Return the User-Agent string for mua. This is either the value
of `mua/user-agent', or, if not set, a string based on the
version of mua and emacs."
(or mua/user-agent
(format "mu %s; emacs %s" (mua/mu-binary-version) emacs-version)))
(defun mua/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
prepending `mua/msg-citation-prefix' to each line."
(let ((from (mua/msg-field msg :from)))
(concat
(format "On %s, %s wrote:"
(format-time-string "%c" (mua/msg-field msg :date))
(if (and from (car from)) ;; a list ((<name> . <email>))
(or (caar from) (cdar from) "someone")
"someone"))
"\n\n"
(replace-regexp-in-string "^" " > "
(mua/msg-body-txt-or-html msg)))))
(defun mua/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))
(defun mua/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\."
(mapconcat
(lambda (recip)
(let ((name (car recip)) (email (cdr recip)))
(if name
(format "%s <%s>" name email)
(format "%s" email)))) lst ", "))
(defun mua/msg-hidden-header (hdr val)
"Return user-invisible header to the message (HDR: VAL\n)."
(format "%s: %s\n" hdr val))
;;(propertize (format "%s: %s\n" hdr val) 'invisible t))
(defun mua/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)))
;;(propertize (format "%s: %s\n" hdr val) 'invisible t))
(defun mua/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 (mua/msg-field msg :references))
(msgid (mua/msg-field msg :message-id)))
(if msgid ;; every received message should have one...
(mapconcat 'identity (append refs (list msgid)) ",")
(mapconcat 'identity refs ","))))
(defun mua/msg-to-create (msg reply-all)
"Construct the To: header for a reply-message based on some
message MSG. If REPLY-ALL is nil, this the the Reply-To addresss
of MSG if it exist, or the From:-address othewise. If reply-all
is non-nil, the To: is what was in the old To: with either the
Reply-To: or From: appended, and then the
receiver (i.e. `user-mail-address') removed.
So:
reply-all nil: Reply-To: or From: of MSG
reply-all t : Reply-To: or From: of MSG + To: of MSG - `user-mail-address'
The result is either nil or a string which can be used for the To:-field."
(let ((to-lst (mua/msg-field msg :to))
(reply-to (mua/msg-field msg :reply-to))
(from (mua/msg-field msg :from)))
(if reply-all
(progn ;; reply-all
(setq to-lst ;; append Reply-To:, or if not set, From: if set
(if reply-to (cons `(nil . ,reply-to) to-lst)
(if from (append to-lst from)
to-lst)))
;; and remove myself from To:
(setq to-lst (mua/msg-recipients-remove to-lst user-mail-address))
(mua/msg-recipients-to-string to-lst))
;; reply single
(progn
(or reply-to (mua/msg-recipients-to-string from))))))
(defconst mua/msg-separator "--text follows this line--\n\n"
"separator between headers and body, needed for `message-mode'")
(defun mua/msg-cc-create (msg reply-all)
"Get the list of Cc-addresses for the reply to MSG. If
REPLY-ALL is nil this is simply empty, otherwise it is the same
list as the one in MSG, 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 (mua/msg-field msg :cc)))
(when (and reply-all cc-lst)
(mu-message-recipients-to-string
(mua/msg-recipients-remove cc-lst
user-mail-address)))))
(defun mua/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))))
(defun mua/msg-create-reply (msg reply-all)
"Create a draft message as a reply to MSG; if REPLY-ALL is
non-nil, reply to all recipients.
A reply message has fields:
From: - see `mu-msg-from-create'
To: - see `mua/msg-to-create'
Cc: - see `mua/msg-cc-create'
Subject: - `mua/msg-reply-prefix' + subject of MSG
then, the following fields, normally hidden from user:
Reply-To: - if `mail-reply-to' has been set
References: - see `mua/msg-references-create'
In-Reply-To: - message-id of MSG
User-Agent - see `mua/msg-user-agent'
Then follows `mua-msg-separator' (for `message-mode' to separate
body from headers)
And finally, the cited body of MSG, as per `mua/msg-cite-original'."
(concat
(mua/msg-header "From" (or (mua/msg-from-create) ""))
(when (boundp 'mail-reply-to)
(mua/msg-header "Reply-To" mail-reply-to))
(mua/msg-header "To" (or (mua/msg-to-create msg reply-all) ""))
(mua/msg-header "Cc" (mua/msg-cc-create msg reply-all))
(mua/msg-hidden-header "User-agent" (mua/msg-user-agent))
(mua/msg-hidden-header "References" (mua/msg-references-create msg))
(mua/msg-hidden-header "In-reply-to" (mua/msg-field msg :message-id))
(mua/msg-header"Subject"
(concat mua/msg-reply-prefix (mua/msg-field msg :subject)))
mua/msg-separator
(mua/msg-cite-original msg)))
;; TODO: attachments
(defun mua/msg-create-forward (msg)
"Create a draft forward message for MSG.
A forward message has fields:
From: - see `mu-msg-from-create'
To: - empty
Subject: - `mua/msg-forward-prefix' + subject of MSG
then, the following fields, normally hidden from user:
Reply-To: - if `mail-reply-to' has been set
References: - see `mua/msg-references-create'
User-Agent - see `mua/msg-user-agent'
Then follows `mua-msg-separator' (for `message-mode' to separate
body from headers)
And finally, the cited body of MSG, as per `mua/msg-cite-original'."
(concat
(mua/msg-header "From" (or (mua/msg-from-for-new) ""))
(when (boundp 'mail-reply-to)
(mua/msg-header "Reply-To" mail-reply-to))
(mua/msg-header "To" "")
(mua/msg-hidden-header "User-agent" (mua/msg-user-agent))
(mua/msg-hidden-header "References" (mua/msg-references-for-reply msg))
(mua/msg-header"Subject"
(concat mua/msg-forward-prefix (mua/msg-field msg :subject)))
mua/msg-separator
(mua/msg-cite-original msg)))
(defun mua/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
User-Agent - see `mua/msg-user-agent'
Then follows `mua-msg-separator' (for `message-mode' to separate
body from headers)."
(concat
(mua/msg-header "From" (or (mua/msg-from-for-new) ""))
(when (boundp 'mail-reply-to)
(mua/msg-header "Reply-To" mail-reply-to))
(mua/msg-header "To" "")
(mua/msg-hidden-header "User-agent" (mua/msg-user-agent))
(mua/msg-header "Subject" "")
mua/msg-separator))
(defconst mua/msg-file-prefix "mua" "prefix for mua-generated
mail files; we use this to ensure that our hooks don't mess
with non-mua-generated messages")
(defun mua/msg-draft-file-name ()
"Create a Maildir-compatible[1], unique file name for a draft
message.
[1]: see http://cr.yp.to/proto/maildir.html"
(format "%s-%x-%x.%s:2,D" ;; 'D': rarely used, but hey, it's available
mua/msg-file-prefix
(format-time-string "%Y%m%d" (current-time))
(emacs-pid)
(random t)
(replace-regexp-in-string "[:/]" "_" (system-name))))
(defun mua/msg-compose (str)
"Create a new draft message in the drafts folder with STR as
its contents, and open this message file for editing
The name of the draft folder is constructed from the concatenation of
`mua/maildir' and `mua/drafts-folder' (therefore, these must be set).
The message file name is a unique name determined by
`mua/msg-draft-file-name'.
The initial STR would be created from either `mua/msg-create-reply',
`mua/msg-create-forward' or `mua/msg-create-new'. The editing buffer is
using Gnus' `message-mode'."
(unless mua/maildir (error "mua/maildir not set"))
(unless mua/drafts-folder (error "mua/drafts-folder not set"))
;; write our draft message to the the drafts folder
(let ((draftfile (concat mua/maildir "/" mua/drafts-folder "/cur/"
(mua/msg-draft-file-name))))
(with-temp-file draftfile (insert str))
(find-file draftfile)
(rename-buffer mua/msg-draft-name t)
(message-mode)
(message-goto-body)))
(defun mua/msg-is-mua-message ()
"Check whether the current buffer refers a mua-message based on
the buffer file name; this is used in hooks we install on
message-mode to ensure we only do things with mua-generated
messages (mua is not the only user of `message-mode' after all)"
(let* ((fname (buffer-file-name))
(match (and fname (string-match mua/msg-file-prefix fname))))
(and (numberp match) (= 0 match))))
;; we simply check if file starts with `mu-msg-file-prefix'
(defun mua/msg-save-to-sent ()
"function that moves the current message to the sent folder"
(if (mua/msg-is-mua-message)
(unless mua/sent-folder (error "mua/sent-folder not set"))
(let ((sent-msg ;; note, the "" parameter remove the D 'Draft'-flag
(mua/msg-move (buffer-file-name) mua/sent-folder "")))
(if (sent-msg) ;; change our buffer file-name
(set-visited-file-name sent-msg t t)
(mua/warn "Failed to save message to the Sent-folder")))))
;; (defun mua/msg-set-replied-flag ()
;; "find the message we replied to, and set its 'Replied' flag."
;; (if (mua/msg-is-mua-message)
;; (let ((msgid (mail-header-parse-addresses
;; (message-field-value "In-Reply-To")))
;; (path (and msgid (shell-command-to-string
;; (concat mua/mu-binary
;; " find msgid:" msgid " --exec=echo | head -1")))))
;; (if path
;; (mu-mv)
;; add-hook
;; add-hook
(provide 'mua-msg)

221
toys/mua/mua-view.el Normal file
View File

@ -0,0 +1,221 @@
;;; mua-view.el -- part of mua, 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:
;; mu
;;; Code:
(eval-when-compile (require 'cl))
(require 'mua-common)
(require 'mua-msg)
(defconst mua/view-buffer-name " *mua-view*"
"buffer name for mua/view buffers")
(defvar mua/view-headers
'(:from :to :cc :subject :flags :date :attachments)
"fields to display in the message view")
(defvar mua/hdrs-buffer nil
"headers buffer for the view")
(defun mua/view (path headersbuf)
"display message at PATH in a new buffer; note that the action
of viewing a message may cause it to be moved/renamed; this
function returns the resulting name. PARENTBUF refers to the
buffer who invoked this view; this allows us to return there when
we quit from this view. Also, if PARENTBUF is a find buffer (ie.,
has mu-headers-mode as its major mode), this allows various
commands (navigation, marking etc.) to be applied to this
buffer."
(let* ((msg (mua/msg-from-path path))
(buf (get-buffer-create mua/view-buffer-name))
(str (mua/view-message msg)))
(when (and msg str)
(switch-to-buffer buf)
(let ((inhibit-read-only t))
(erase-buffer)
(insert str))
(mua/view-mode)
(setq ;; these are buffer-local
mua/hdrs-buffer headersbuf
mua/parent-buffer headersbuf)
(goto-char (point-min)))))
(defun mua/view-message (msg)
"construct a display string for the message"
(let ((hdrs
(mapconcat
(lambda (field)
(case field
(:subject (mua/view-header msg "Subject" :subject))
(:path (mua/view-header msg "Path" :path))
(:to (mua/view-contacts msg field))
(:from (mua/view-contacts msg field))
(:cc (mua/view-contacts msg field))
(:bcc (mua/view-contacts msg field))
(:date (mua/view-date msg))
(:flags (mua/view-flags msg))
(:size (mua/view-size msg))
(:attachments (mua/view-attachments msg))
(t (error "Unsupported field: %S" field))))
mua/view-headers ""))
(body (mua/msg-body-txt-or-html msg)))
(concat hdrs "\n" body)))
(defun mua/view-header-string (key val face)
(if val
(concat
(propertize key 'face 'mua/header-title-face) ": "
(propertize val 'face face) "\n")
""))
(defun mua/view-header (msg key field)
"show header FIELD for MSG with KEY. ie. <KEY>: value-of-FIELD\n"
(mua/view-header-string key (mua/msg-field msg field) 'mua/header-face))
(defun mua/view-contacts (msg field)
(unless (member field '(:to :from :bcc :cc))
(error "Illegal type for contact"))
(let* ((lst (mua/msg-field msg field))
(contacts
(when lst
(mapconcat
(lambda(c) (let ((name (car c)) (email (cdr c)))
(if name
(format "%s <%s>" name email)
(format "%s" email)))) lst ", "))))
(if contacts
(mua/view-header-string
(case field (:to "To") (:from "From") (:bcc "Bcc") (:cc "Cc"))
contacts 'mua/contacts-face)
"")))
(defun mua/view-date (msg)
(let* ((date (mua/msg-field msg :date))
(datestr (when date (format-time-string "%c" date))))
(mua/view-header-string "Date" datestr 'mua/header-face)))
(defun mua/view-size (msg)
(let* ((size (mua/msg-field msg :size))
(sizestr (when size (format "%d bytes"))))
(mua/view-header-string "Size" sizestr 'mua-header-face)))
(defun mua/view-flags (msg)
""
"" ;; todo
)
(defun mua/view-attachments (msg)
""
"" ;; todo
)
(defvar mua/view-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" 'mua/quit-buffer)
(define-key map "s" 'mua/view-search)
(define-key map "f" 'mua/view-forward)
(define-key map "r" 'mua/view-reply)
(define-key map "c" 'mua/view-compose)
;; navigation between messages
(define-key map "n" 'mua/view-next)
(define-key map "p" 'mua/view-prev)
;; marking/unmarking
(define-key map "d" '(lambda (mua/view-mark 'trash)))
(define-key map "D" '(lambda (mua/view-mark 'delete)))
(define-key map "m" '(lambda (mua/view-mark 'move)))
(define-key map "u" '(lambda (mua/view-mark 'unmark)))
(define-key map "x" 'mua/view-marked-execute)
map)
"Keymap for \"*mua-view*\" buffers.")
(fset 'mua/view-mode-map mua/view-mode-map)
(defun mua/view-mode ()
"major mode for viewing an e-mail message"
(interactive)
(kill-all-local-variables)
(use-local-map mua/view-mode-map)
(make-local-variable 'mua/parent-buffer)
(make-local-variable 'mua/hdrs-buffer)
(make-local-variable 'mua/path)
(setq major-mode 'mua/view-mode mode-name "*mu-view*")
(setq truncate-lines t buffer-read-only t))
(defmacro mua/with-hdrs-buffer (&rest body)
"Execute the forms in BODY with the mua/hdrs-buffer temporarily current.
Note that this actually switches the buffer, and changes to point
etc. persist."
(declare (indent 1) (debug t))
`(let ((oldbuf (current-buffer)))
(if (buffer-live-p mua/hdrs-buffer)
(progn
(set-buffer mua/hdrs-buffer)
(progn ,@body)
(set-buffer oldbuf))
(mua/warn "hdrs buffer is dead"))))
(defun mua/view-next ()
"move to the next message; note, this will replace the current
buffer"
(interactive)
(mua/with-hdrs-buffer
(when (mua/hdrs-next) (mua/hdrs-view))))
(defun mua/view-prev ()
"move to the previous message; note, this will replace the
current buffer"
(interactive)
(mua/with-hdrs-buffer
(when (mua/hdrs-prev) (mua/hdrs-view))))
(defun mua/view-reply ()
"Reply to the current message."
(interactive) (mua/with-hdrs-buffer (mua/hdrs-reply)))
(defun mua/view-forward ()
"Reply to the current message."
(interactive) (mua/with-hdrs-buffer (mua/hdrs-forward)))
(defun mua/view-compose ()
"Write a new message."
(interactive) (mua/with-hdrs-buffer (mua/hdrs-compose)))
(provide 'mua-view)