* some code re-arrangement: src/guile-> guile, toys/mm -> emacs

This commit is contained in:
djcb
2011-12-13 08:03:19 +02:00
parent eb7bd05487
commit 8b39c69c89
31 changed files with 10 additions and 22 deletions

35
emacs/Makefile.am Normal file
View File

@ -0,0 +1,35 @@
## Copyright (C) 2008-2011 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
##
## This program 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.
##
## This program 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 this program; if not, write to the Free Software Foundation,
## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
include $(top_srcdir)/gtest.mk
SUBDIRS=
BUILT_SOURCES=mm-version.el
mm-version.el:
@echo -e ";; auto-generated\n\
(defconst mm/mu-version \"$(VERSION)\" \"Required mu binary version.\")\n\
(provide 'mm-version)\n" >$@
EXTRA_DIST= \
mm.el \
mm-hdrs.el \
mm-view.el \
mm-proc.el \
mm-send.el \
mm-version.el

66
emacs/TODO Normal file
View File

@ -0,0 +1,66 @@
* TODO
** Bugs
*** forward should take the attachments from the original
*** database locks
** Features i
*** documentation
*** emacs install
** Features ii
*** mark thread
*** bounce support
*** sorting
*** recursive ask-maildir
*** display unread info at startup
*** tool bars
*** make links clickable
*** colorize cited parts in view
*** refiling-by-pattern
*** window management
*** inspect message (muile)
*** message statistics
*** include exchange handling / org integration
*** integrate with org-contacts, bbdb
* DONE
** don't put updated messages at the end
** mark region
** editing drafts
** combine download / re-index
** toggleable message line wrapping in view
** threading problems
** missing? message
** double messages (different docids though)
** sometimes commands are not executed immediately
** menus
** flag information for content flags (encrypted, signed, has-attach)
** use top bar
** add footer to search results
** q while adding headers
** handle start/stopping server
** hostname in draftname
** make quick folders work for both jump and move
** synchronize headers / view buffer point
** make date format customizable
** add :from-or-to
** fix to:/cc: etc.
** give info about target for move
** --maildir=~/Maildir does not work anymore
** message threading
** view raw message
** Sent Items
** raw-mode / quit
** customizable bookmarks
** fix queued sending
** version check at startup
# Local Variables:
# mode: org; org-startup-folded: nil
# End:

779
emacs/mm-hdrs.el Normal file
View File

@ -0,0 +1,779 @@
;; mm-hdrs.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 are function related to creating the list of one-line
;; descriptions of emails, aka 'headers' (not to be confused with headers like
;; 'To:' or 'Subject:')
;; mm
;; Code:
(eval-when-compile (require 'cl))
(require 'mm-proc)
;;;; internal variables/constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar mm/last-expr nil
"*internal* The most recent search expression.")
(defvar mm/sortfield nil
"*internal* Field to sort headers by")
(defvar mm/sort-descending nil
"*internal Whether to sort in descending order")
(defconst mm/hdrs-buffer-name "*mm-headers*"
"*internal* Name of the buffer for message headers.")
(defvar mm/hdrs-buffer nil
"*internal* Buffer for message headers")
(defun mm/hdrs-search (expr &optional full-search)
"Search in the mu database for EXPR, and switch to the output
buffer for the results. If FULL-SEARCH is non-nil return all
results, otherwise, limit number of results to
`mm/search-results-limit'."
(let ((buf (get-buffer-create mm/hdrs-buffer-name))
(inhibit-read-only t))
(with-current-buffer buf
(erase-buffer)
(mm/hdrs-mode)
(setq
mm/mm/marks-map nil
mm/msg-map (make-hash-table :size 1024 :rehash-size 2 :weakness nil)
mode-name expr
mm/last-expr expr
mm/hdrs-buffer buf)))
(switch-to-buffer mm/hdrs-buffer)
(mm/proc-find expr ;; '-1' means 'unlimited search'
(if full-search -1 mm/search-results-limit)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; handler functions
;;
;; next are a bunch of handler functions; those will be called from mm-proc in
;; response to output from the server process
(defun mm/hdrs-view-handler (msg)
"Handler function for displaying a message."
(mm/view msg mm/hdrs-buffer))
(defun mm/hdrs-error-handler (err)
"Handler function for showing an error."
(let ((errcode (plist-get err :error))
(errmsg (plist-get err :error-message)))
(case errcode
(4 (message "No matches for this search query."))
(t (message (format "Error %d: %s" errcode errmsg))))))
(defun mm/hdrs-update-handler (msg is-move)
"Update handler, will be called when a message has been updated
in the database. This function will update the current list of
headers."
(when (buffer-live-p mm/hdrs-buffer)
(with-current-buffer mm/hdrs-buffer
(let* ((docid (plist-get msg :docid))
(marker (gethash docid mm/msg-map))
(point (when marker (marker-position marker))))
(when point ;; is the message present in this list?
;; if it's marked, unmark it now
(when (mm/hdrs-docid-is-marked docid) (mm/hdrs-mark 'unmark))
;; first, remove the old one (otherwise, we'd have to headers with
;; the same docid...
(mm/hdrs-remove-handler docid)
;; if we we're actually viewing this message (in mm/view mode), we
;; update the `mm/current-msg' there as well; that way, the flags can
;; be updated, as well as the path (which is useful for viewing the
;; raw message)
(let ((viewbuf (get-buffer mm/view-buffer-name)))
(when (and viewbuf (buffer-live-p viewbuf))
(with-current-buffer viewbuf
(when (eq docid (plist-get mm/current-msg :docid))
(setq mm/current-msg msg)))))
;; now, if this update was about *moving* a message, we don't show it
;; anymore (of course, we cannot be sure if the message really no
;; longer matches the query, but this seem a good heuristic.
;; if it was only a flag-change, show the message with its updated flags.
(unless is-move
(mm/hdrs-header-handler msg point)))))))
(defun mm/hdrs-remove-handler (docid)
"Remove handler, will be called when a message has been removed
from the database. This function will hide the remove message in
the current list of headers."
(with-current-buffer mm/hdrs-buffer
(let* ((marker (gethash docid mm/msg-map))
(pos (and marker (marker-position marker)))
(docid-at-pos (and pos (mm/hdrs-get-docid pos))))
(unless marker (error "Message %d not found" docid))
(unless (eq docid docid-at-pos)
(error "At point %d, expected docid %d, but got %d"
pos docid docid-at-pos))
(mm/hdrs-remove-header docid pos))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm/hdrs-contact-str (contacts)
"Turn the list of contacts CONTACTS (with elements (NAME . EMAIL)
into a string."
(mapconcat
(lambda (ct)
(let ((name (car ct)) (email (cdr ct)))
(or name email "?"))) contacts ", "))
(defun mm/thread-prefix (thread)
"Calculate the thread prefix based on thread info THREAD."
(if thread
(let ( (level (plist-get thread :level))
(first-child (plist-get thread :first-child))
(has-child (plist-get thread :has-child))
(duplicate (plist-get thread :duplicate))
(empty-parent (plist-get thread :empty-parent)))
(concat
(make-string (* (if empty-parent 0 2) level) ?\s)
(cond
(has-child "+ ")
(empty-parent "- ")
(first-child "\\ ")
(duplicate "= ")
(t "| "))))))
;; FIXME: when updating an header line, we don't know the thread
;; stuff
(defun mm/hdrs-header-handler (msg &optional point)
"Create a one line description of MSG in this buffer, at POINT,
if provided, or at the end of the buffer otherwise."
(let* ( (docid (plist-get msg :docid))
(thread-info
(or (plist-get msg :thread) (gethash docid mm/thread-info-map)))
(line
(mapconcat
(lambda (f-w)
(let* ((field (car f-w)) (width (cdr f-w))
(val (plist-get msg field))
(str
(case field
(:subject (concat (mm/thread-prefix thread-info) val))
((:maildir :path) val)
((:to :from :cc :bcc) (mm/hdrs-contact-str val))
;; if we (ie. `user-mail-address' is the 'From', show
;; 'To', otherwise show From
(:from-or-to
(let* ((from-lst (plist-get msg :from))
(from (and from-lst (cdar from-lst))))
(if (and from (string-match
mm/user-mail-address-regexp from))
(concat "To "
(mm/hdrs-contact-str (plist-get msg :to)))
(mm/hdrs-contact-str from-lst))))
(:date (format-time-string mm/headers-date-format val))
(:flags (mm/flags-to-string val))
(:size (mm/display-size val))
(t (error "Unsupported header field (%S)" field)))))
(when str
(if (not width)
str
(truncate-string-to-width str width 0 ?\s t)))))
mm/headers-fields " "))
(flags (plist-get msg :flags))
(line (cond
((member 'draft flags)
(propertize line 'face 'mm/draft-face 'draft t))
((member 'trashed flags)
(propertize line 'face 'mm/trashed-face))
((member 'unread flags)
(propertize line 'face 'mm/unread-face))
(t ;; else
(propertize line 'face 'mm/header-face)))))
;; store the thread info, so we can use it when updating the message
(when thread-info
(puthash docid thread-info mm/thread-info-map))
(mm/hdrs-add-header line (plist-get msg :docid)
(if point point (point-max)))))
(defun mm/hdrs-found-handler (count)
"Create a one line description of the number of headers found
after the end of the search results."
(with-current-buffer mm/hdrs-buffer
(save-excursion
(goto-char (point-max))
(let ((inhibit-read-only t))
(insert (propertize
(case count
(0 "No matching messages found")
;; note, don't show the number so we don't have to update it
;; when we delete messsages...
(otherwise "End of search results"))
;; (1 "Found 1 message")
;; (otherwise (format "Found %d messages" count)))
'face 'mm/system-face 'intangible t))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; hdrs-mode and mode-map ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(setq mm/hdrs-mode-map nil)
(defvar mm/hdrs-mode-map nil
"Keymap for *mm-headers* buffers.")
(unless mm/hdrs-mode-map
(setq mm/hdrs-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "s" 'mm/search)
(define-key map "S" 'mm/search-full)
(define-key map "b" 'mm/search-bookmark)
(define-key map "q" 'mm/quit-buffer)
;; (define-key map "o" 'mm/change-sort)
(define-key map "g" 'mm/rerun-search)
;; navigation
(define-key map "n" 'mm/next-header)
(define-key map "p" 'mm/prev-header)
;; marking/unmarking/executing
(define-key map (kbd "<backspace>") 'mm/mark-for-trash)
(define-key map "d" 'mm/mark-for-trash)
(define-key map (kbd "<delete>") 'mm/mark-for-delete)
(define-key map "D" 'mm/mark-for-delete)
(define-key map "j" 'mm/jump-to-maildir)
(define-key map "m" 'mm/mark-for-move)
(define-key map "u" 'mm/unmark)
(define-key map "U" 'mm/unmark-all)
(define-key map "x" 'mm/execute-marks)
;; message composition
(define-key map "r" 'mm/compose-reply)
(define-key map "f" 'mm/compose-forward)
(define-key map "c" 'mm/compose-new)
(define-key map "e" 'mm/edit-draft)
(define-key map (kbd "RET") 'mm/view-message)
;; menu
(define-key map [menu-bar] (make-sparse-keymap))
(let ((menumap (make-sparse-keymap "Headers")))
(define-key map [menu-bar headers] (cons "Headers" menumap))
(define-key menumap [quit-buffer] '("Quit view" . mm/quit-buffer))
(define-key menumap [sepa0] '("--"))
(define-key menumap [execute-marks] '("Execute marks" . mm/execute-marks))
(define-key menumap [unmark-all] '("Unmark all" . mm/unmark-all))
(define-key menumap [unmark] '("Unmark" . mm/unmark))
(define-key menumap [mark-delete] '("Mark for deletion" . mm/mark-for-delete))
(define-key menumap [mark-trash] '("Mark for trash" . mm/mark-for-trash))
(define-key menumap [mark-move] '("Mark for move" . mm/mark-for-move))
(define-key menumap [sepa1] '("--"))
(define-key menumap [compose-new] '("Compose new" . mm/compose-new))
(define-key menumap [forward] '("Forward" . mm/compose-forward))
(define-key menumap [reply] '("Reply" . mm/compose-reply))
(define-key menumap [sepa2] '("--"))
(define-key menumap [refresh] '("Refresh" . mm/rerun-search))
(define-key menumap [search] '("Search" . mm/search))
(define-key menumap [search-full] '("Search full" . mm/search-full))
(define-key menumap [jump] '("Jump to maildir" . mm/jump-to-maildir))
(define-key menumap [sepa3] '("--"))
(define-key menumap [view] '("View" . mm/view-message))
(define-key menumap [next] '("Next" . mm/next-header))
(define-key menumap [previous] '("Previous" . mm/prev-header))
(define-key menumap [sepa4] '("--")))
;;(define-key menumap [draft] '("Edit draft" . mm/compose-new))
map)))
(fset 'mm/hdrs-mode-map mm/hdrs-mode-map)
(defun mm/hdrs-mode ()
"Major mode for displaying mua search results."
(interactive)
(kill-all-local-variables)
(use-local-map mm/hdrs-mode-map)
(make-local-variable 'mm/last-expr)
(make-local-variable 'mm/hdrs-proc)
(make-local-variable 'mm/marks-map)
(make-local-variable 'mm/msg-map)
(make-local-variable 'mm/thread-info-map)
;; we register our handler functions for the mm-proc (mu server) output
(setq mm/proc-error-func 'mm/hdrs-error-handler)
(setq mm/proc-update-func 'mm/hdrs-update-handler)
(setq mm/proc-header-func 'mm/hdrs-header-handler)
(setq mm/proc-found-func 'mm/hdrs-found-handler)
(setq mm/proc-view-func 'mm/hdrs-view-handler)
(setq mm/proc-remove-func 'mm/hdrs-remove-handler)
;; this last one is defined in mm-send.el
(setq mm/proc-compose-func 'mm/send-compose-handler)
(setq
mm/marks-map (make-hash-table :size 16 :rehash-size 2)
mm/thread-info-map (make-hash-table :size 512 :rehash-size 2)
major-mode 'mm/hdrs-mode
mode-name "mm: message headers"
truncate-lines t
buffer-read-only t
overwrite-mode 'overwrite-mode-binary)
(setq header-line-format
(cons "* "
(map 'list
(lambda (item) ;; FIXME
(let ((field (cdr (assoc (car item) mm/header-names)))
(width (cdr item)))
(concat
(propertize
(if width
(truncate-string-to-width field width 0 ?\s t)
field)
'face 'mm/title-face) " ")))
mm/headers-fields))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar mm/msg-map nil
"*internal* A map (hashtable) which maps a database (Xapian)
docid (which uniquely identifies a message to a marker. where
marker points to the buffer position for the message.
Using this map, we can update message headers which are currently
on the screen, when we receive (:update ) notices from the mu
server.")
(defun mm/hdrs-add-header (str docid point)
"Add header STR with DOCID to the buffer at POINT."
(unless docid (error "Invalid message"))
(when (buffer-live-p mm/hdrs-buffer)
(with-current-buffer mm/hdrs-buffer
(let ((inhibit-read-only t))
(save-excursion
(goto-char point)
;; Update `mm/msg-map' with MSG, and MARKER pointing to the buffer
;; position for the message header."
(insert (propertize (concat " " str "\n") 'docid docid))
(puthash docid (copy-marker point t) mm/msg-map))))))
(defun mm/hdrs-remove-header (docid point)
"Remove header with DOCID at POINT."
(with-current-buffer mm/hdrs-buffer
(goto-char point)
;; sanity check
(unless (eq docid (mm/hdrs-get-docid))
(error "%d: Expected %d, but got %d"
(line-number-at-pos) docid (mm/hdrs-get-docid)))
(let ((inhibit-read-only t))
;; (put-text-property (line-beginning-position line-beginning-positio 2)
;; 'invisible t))
(delete-region (line-beginning-position) (line-beginning-position 2)))
(remhash docid mm/msg-map)))
(defun mm/hdrs-mark-header (docid mark)
"(Visually) mark the header for DOCID with character MARK."
(let ((marker (gethash docid mm/msg-map)))
;; (unless marker (error "Unregistered message"))
(when marker
(with-current-buffer mm/hdrs-buffer
(save-excursion
(let ((inhibit-read-only t) (pos (marker-position marker)))
(goto-char pos)
(delete-char 2)
(insert (propertize mark 'face 'mm/hdrs-marks-face) " ")
(put-text-property pos
(line-beginning-position 2) 'docid docid)
;; update the msg-map, ie., move it back to the start of the line
(puthash docid
(copy-marker (line-beginning-position) t)
mm/msg-map)))))))
(defun mm/hdrs-get-docid (&optional point)
"Get the docid for the message at POINT, if provided, or (point), otherwise."
(with-current-buffer mm/hdrs-buffer
(get-text-property (if point point (point)) 'docid)))
(defun mm/dump-msg-map ()
"*internal* dump the message map (for debugging)."
(with-current-buffer mm/hdrs-buffer
(message "msg-map (%d)" (hash-table-count mm/msg-map))
(maphash
(lambda (k v)
(message "%s => %s" k v))
mm/msg-map)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; threadinfo-map ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar mm/thread-info-map nil
"Map (hash) of docid->threadinfo; when filling the list of
messages, we fill a map of thread info, such that when a header
changes (e.g., it's read-flag gets set) through some (:update
...) message, we can restore the thread-info (this is needed
since :update messages do not include thread info).")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; marks ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar mm/marks-map nil
"Map (hash) of docid->markinfo; when a message is marked, the
information is added here.
markinfo is a list consisting of the following:
\(marker mark target)
where
MARKER is an emacs-textmarker pointing to the beginning of the header line
MARK is the type of mark (move, trash, delete)
TARGET (optional) is the target directory (for 'move')")
(defun mm/hdrs-mark-message (mark &optional target)
"Mark (or unmark) message at point. MARK specifies the
mark-type. For `move'-marks there is also the TARGET argument,
which specifies to which maildir the message is to be moved.
The following marks are available, and the corresponding props:
MARK TARGET description
----------------------------------------------------------
`move' y move the message to some folder
`trash' n move the message to `mm/trash-folder'
`delete' n remove the message
`unmark' n unmark this message"
(let* ((docid (mm/hdrs-get-docid))
(markkar
(case mark ;; the visual mark
('move "m")
('trash "d")
('delete "D")
('select "*")
('unmark " ")
(t (error "Invalid mark %S" mark)))))
(unless docid (error "No message on this line"))
(save-excursion
(when (mm/hdrs-mark-header docid markkar))
;; update the hash -- remove everything current, and if add the new stuff,
;; unless we're unmarking
(remhash docid mm/marks-map)
;; remove possible overlays
(remove-overlays (line-beginning-position) (line-end-position))
;; now, let's set a mark (unless we were unmarking)
(unless (eql mark 'unmark)
(puthash docid (list (point-marker) mark target) mm/marks-map)
;; when we have a target (ie., when moving), show the target folder in
;; an overlay
(when target
(let* ((targetstr (propertize (concat "-> " target " ")
'face 'mm/system-face))
(start (+ 2 (line-beginning-position))) ;; +2 for the marker fringe
(overlay (make-overlay start (+ start (length targetstr)))))
(overlay-put overlay 'display targetstr)))))))
(defun mm/hdrs-mark (mark &optional target)
"Mark the header at point, or, if
region is active, mark all headers in the region. Als see
`mm/hdrs-mark-message'."
(with-current-buffer mm/hdrs-buffer
(if (use-region-p)
;; mark all messages in the region.
(save-excursion
(let ((b (region-beginning)) (e (region-end)))
(goto-char b)
(while (<= (line-beginning-position) e)
(mm/hdrs-mark-message mark target)
(forward-line 1))))
;; just a single message
(mm/hdrs-mark-message mark target))))
(defun mm/hdrs-marks-execute ()
"Execute the actions for all marked messages in this
buffer. After the actions have been executed succesfully, the
affected messages are *hidden* from the current header list. Since
the headers are the result of a search, we cannot be certain that
the messages no longer matches the current one - to get that
certainty, we need to rerun the search, but we don't want to do
that automatically, as it may be too slow and/or break the users
flow. Therefore, we hide the message, which in practice seems to
work well."
(if (= 0 (hash-table-count mm/marks-map))
(message "Nothing is marked")
(maphash
(lambda (docid val)
(let ((marker (nth 0 val)) (mark (nth 1 val)) (target (nth 2 val)))
(case mark
(move
(mm/proc-move-msg docid target))
(trash
(unless mm/trash-folder
(error "`mm/trash-folder' not set"))
(mm/proc-move-msg docid mm/trash-folder "+T"))
(delete
(mm/proc-remove-msg docid)))))
mm/marks-map)
(mm/hdrs-unmark-all)))
(defun mm/hdrs-unmark-all ()
"Unmark all marked messages."
(unless (/= 0 (hash-table-count mm/marks-map))
(error "Nothing is marked"))
(maphash
(lambda (docid val)
(save-excursion
(goto-char (marker-position (nth 0 val)))
(mm/hdrs-mark 'unmark)))
mm/marks-map))
(defun mm/hdrs-view ()
"View message at point."
(let ((docid (mm/hdrs-get-docid)))
(unless docid (error "No message at point."))
(mm/proc-view-msg docid)))
(defun mm/hdrs-compose (compose-type)
"Compose either a reply/forward based on the message at point. or
start editing it. COMPOSE-TYPE is either `reply', `forward' or
`edit'."
(if (eq compose-type 'new)
(mm/send-compose-handler 'new)
(let ((docid (mm/hdrs-get-docid))
;; note, the first two chars of the line (the mark margin) does *not*
;; have the 'draft property; thus, we check one char before the end of
;; the current line instead
(is-draft (get-text-property (- (line-end-position) 1) 'draft)))
(unless docid
(error "No message at point."))
(cond
((member compose-type '(reply forward))
(mm/proc-compose compose-type docid))
((eq compose-type 'edit)
(unless is-draft
(error "Cannot edit a non-draft message"))
(mm/proc-compose 'edit docid))
(t (error "invalid compose type %S" compose-type))))))
(defun mm/hdrs-docid-is-marked (docid)
"Is the given docid marked?"
(when (gethash docid mm/marks-map) t))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; interactive functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm/ignore-marks ()
(let*
((num
(hash-table-count mm/marks-map))
(unmark (or (= 0 num)
(y-or-n-p
(format "Sure you want to unmark %d message(s)?" num)))))
(message nil)
unmark))
(defun mm/search (expr)
"Start a new mu search, limited to `mm/search-results-limit'
results."
(interactive "s[mu] search for: ")
(when (mm/ignore-marks) (mm/hdrs-search expr)))
(defun mm/search-full (expr)
"Start a new mu search; resturn *all* results."
(interactive "s[mu] full search for: ")
(when (mm/ignore-marks)
(mm/hdrs-search expr t)))
(defun mm/search-bookmark ()
"Search using some bookmarked query."
(interactive)
(let ((query (mm/ask-bookmark "Bookmark: ")))
(when query
(mm/hdrs-search query))))
(defun mm/quit-buffer ()
"Quit the current buffer."
(interactive)
(when (mm/ignore-marks)
(mm/kill-proc) ;; hmmm...
(kill-buffer)
(mm)))
(defun mm/rerun-search ()
"Rerun the search for the last search expression; if none exists,
do a new search."
(interactive)
(when (mm/ignore-marks)
(if mm/last-expr
(mm/hdrs-search mm/last-expr)
(mm/search))))
(defun mm/view-message ()
"View the message at point."
(interactive)
(mm/hdrs-view))
(defun mm/next-header ()
"Move point to the next message header. If this succeeds, return
the new docid. Otherwise, return nil."
(interactive)
(with-current-buffer mm/hdrs-buffer
(when (= 0 (forward-line 1))
(or (mm/hdrs-get-docid) (mm/next-header)) ;; skip non-headers
;; trick to move point, even if this function is called when this window
;; is not visible
(set-window-point (get-buffer-window mm/hdrs-buffer) (point)))))
(defun mm/prev-header ()
"Move point to the previous message header. If this succeeds,
return the new docid. Otherwise, return nil."
(interactive)
(with-current-buffer mm/hdrs-buffer
(when (= 0 (forward-line -1))
(or (mm/hdrs-get-docid) (mm/prev-header)) ;; skip non-headers
;; trick to move point, even if this function is called when this window
;; is not visible
(set-window-point (get-buffer-window mm/hdrs-buffer) (point)))))
(defun mm/jump-to-maildir ()
"Show the messages in maildir TARGET. If TARGET is not provided,
ask user for it."
(interactive)
(let ((fld (mm/ask-maildir "Jump to maildir: ")))
(when fld
(mm/hdrs-search (concat "maildir:" fld)))))
(defun mm/mark-for-move (&optional target)
"Mark message at point for moving to maildir TARGET. If target is
not provided, function asks for it."
(interactive)
(with-current-buffer mm/hdrs-buffer
(let* ((target (or target (mm/ask-maildir "Move message to: ")))
(fulltarget (concat mm/maildir target)))
(when (or (file-directory-p fulltarget)
(and (yes-or-no-p
(format "%s does not exist. Create now?" fulltarget))
(mm/proc-mkdir fulltarget)))
(mm/hdrs-mark 'move target)
(mm/next-header)))))
(defun mm/mark-for-trash ()
"Mark message at point for moving to the trash
folder (`mm/trash-folder')."
(interactive)
(unless mm/trash-folder
(error "`mm/trash-folder' is not set"))
(with-current-buffer mm/hdrs-buffer
(mm/hdrs-mark 'trash)
(mm/next-header)))
(defun mm/mark-for-delete ()
"Mark message at point for direct deletion."
(interactive)
(with-current-buffer mm/hdrs-buffer
(mm/hdrs-mark 'delete)
(mm/next-header)))
(defun mm/unmark ()
"Unmark message at point."
(interactive)
(with-current-buffer mm/hdrs-buffer
(mm/hdrs-mark 'unmark)
(mm/next-header)))
(defun mm/unmark-all ()
"Unmark all messages."
(interactive)
(with-current-buffer mm/hdrs-buffer
(if (= 0 (hash-table-count mm/marks-map))
(message "Nothing is marked")
(when (mm/ignore-marks)
(mm/hdrs-unmark-all)))))
(defun mm/execute-marks ()
"Execute the actions for the marked messages."
(interactive)
(with-current-buffer mm/hdrs-buffer
(if (= 0 (hash-table-count mm/marks-map))
(message "Nothing is marked")
(when (y-or-n-p (format "Sure you want to execute marks on %d message(s)?"
(hash-table-count mm/marks-map)))
(mm/hdrs-marks-execute)
(message nil)))))
(defun mm/compose-reply ()
"Start composing a reply to the current message."
(interactive)
(with-current-buffer mm/hdrs-buffer
(mm/hdrs-compose 'reply)))
(defun mm/compose-forward ()
"Start composing a forward to the current message."
(interactive)
(with-current-buffer mm/hdrs-buffer
(mm/hdrs-compose 'forward)))
(defun mm/compose-new ()
"Compose a new, empty message."
(interactive)
(mm/hdrs-compose 'new))
(defun mm/edit-draft ()
"Start editing the existing draft message at point."
(interactive)
(with-current-buffer mm/hdrs-buffer
(mm/hdrs-compose 'edit)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'mm-hdrs)

142
emacs/mm-main.el Normal file
View File

@ -0,0 +1,142 @@
;;; mm-main.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:
;;; Code:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; mm main view mode + keybindings
(defconst mm/main-buffer-name "*mm*"
"*internal* Name of the mm main view buffer.")
(defvar mm/mm-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "b" 'mm/search-bookmark)
(define-key map "s" 'mm/search)
(define-key map "S" 'mm/search-full)
(define-key map "q" 'mm/quit-mm)
(define-key map "j" 'mm/jump-to-maildir)
(define-key map "c" 'mm/compose-new)
(define-key map "m" 'mm/toggle-mail-sending-mode)
(define-key map "f" 'smtpmail-send-queued-mail)
(define-key map "u" 'mm/retrieve-mail-update-db)
map)
"Keymap for the *mm* buffer.")
(fset 'mm/mm-mode-map mm/mm-mode-map)
(defun mm/mm-mode ()
"Major mode for the mm main screen."
(interactive)
(kill-all-local-variables)
(use-local-map mm/mm-mode-map)
(setq
mm/marks-map (make-hash-table :size 16 :rehash-size 2)
major-mode 'mm/mm-mode
mode-name "mm: main view"
truncate-lines t
buffer-read-only t
overwrite-mode 'overwrite-mode-binary))
(defun mm/action-str (str)
"Highlight the first occurence of [..] in STR."
(if (string-match "\\[\\(\\w+\\)\\]" str)
(let* ((key (match-string 1 str))
(keystr (propertize key 'face 'mm/highlight-face)))
(replace-match keystr nil t str 1))
str))
(defun mm/main-view()
"Show the mm main view."
(let ((buf (get-buffer-create mm/main-buffer-name))
(inhibit-read-only t))
(with-current-buffer buf
(erase-buffer)
(insert
"* "
(propertize "mm - mu mail for emacs version " 'face 'mm/title-face)
(propertize mm/mu-version 'face 'mm/view-header-key-face)
"\n\n"
(propertize " Basics\n\n" 'face 'mm/title-face)
(mm/action-str "\t* [j]ump to some maildir\n")
(mm/action-str "\t* enter a [s]earch query\n")
(mm/action-str "\t* [c]ompose a new message\n")
"\n"
(propertize " Bookmarks\n\n" 'face 'mm/title-face)
(mapconcat
(lambda (bm)
(let* ((query (nth 0 bm)) (title (nth 1 bm)) (key (nth 2 bm)))
(mm/action-str
(concat "\t* [b" (make-string 1 key) "] " title))))
mm/bookmarks "\n")
"\n"
(propertize " Misc\n\n" 'face 'mm/title-face)
(mm/action-str "\t* [u]pdate email & database\n")
(mm/action-str "\t* toggle [m]ail sending mode ")
"(" (propertize (if smtpmail-queue-mail "queued" "direct")
'face 'mm/view-header-key-face) ")\n"
(mm/action-str "\t* [f]lush queued mail\n")
"\n"
(mm/action-str "\t* [q]uit mm\n"))
(mm/mm-mode)
(switch-to-buffer buf))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactive functions
(defun mm/retrieve-mail-update-db ()
"Get new mail and update the database."
(interactive)
(mm/proc-retrieve-mail-update-db))
(defun mm/toggle-mail-sending-mode ()
"Toggle sending mail mode, either queued or direct."
(interactive)
(setq smtpmail-queue-mail (not smtpmail-queue-mail))
(message
(if smtpmail-queue-mail
"Outgoing mail will now be queued"
"Outgoing mail will now be sent directly"))
(mm))
(defun mm/quit-mm()
"Quit the mm session."
(interactive)
(when (y-or-n-p "Are you sure you want to quit mm? ")
(message nil)
(mm/kill-proc)
(kill-buffer)))
(provide 'mm-main)

450
emacs/mm-proc.el Normal file
View File

@ -0,0 +1,450 @@
;;; mm-proc.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:
;;; Code:
(eval-when-compile (require 'cl))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; internal vars
(defvar mm/mu-proc nil
"*internal* The mu-server process")
(defvar mm/proc-error-func 'mm/default-handler
"*internal* A function called for each error returned from the
server process; the function is passed an error plist as
argument. See `mm/proc-filter' for the format.")
(defvar mm/proc-update-func 'mm/default-handler
"*internal* A function called for each :update sexp returned from
the server process; the function is passed a msg sexp as
argument. See `mm/proc-filter' for the format.")
(defvar mm/proc-remove-func 'mm/default-handler
"*internal* A function called for each :remove sexp returned from
the server process, when some message has been deleted. The
function is passed the docid of the removed message.")
(defvar mm/proc-view-func 'mm/default-handler
"*internal* A function called for each single message sexp
returned from the server process. The function is passed a message
sexp as argument. See `mm/proc-filter' for the
format.")
(defvar mm/proc-header-func 'mm/default-handler
"*internal* A function called for each message returned from the
server process; the function is passed a msg plist as argument. See
`mm/proc-filter' for the format.")
(defvar mm/proc-found-func 'mm/default-handler
"*internal* A function called for when we received a :found sexp
after the headers have returns, to report on the number of
matches. See `mm/proc-filter' for the format.")
(defvar mm/proc-compose-func 'mm/default-handler
"*internal* A function called for each message returned from the
server process that is used as basis for composing a new
message (ie., either a reply or a forward); the function is passed
msg and a symbol (either reply or forward). See `mm/proc-filter'
for the format of <msg-plist>.")
(defvar mm/proc-info-func 'mm/default-handler
"*internal* A function called for each (:info type ....) sexp
received from the server process.")
(defvar mm/proc-pong-func 'mm/default-handler
"*internal* A function called for each (:pong type ....) sexp
received from the server process.")
(defvar mm/buf nil
"*internal* Buffer for results data.")
(defvar mm/path-docid-map
(make-hash-table :size 32 :rehash-size 2 :test 'equal :weakness nil)
"*internal* hash we use to keep a path=>docid mapping for message
we added ourselves (ie., draft messages), so we can e.g. move them
to the sent folder using their docid")
(defun mm/proc-info-handler (info)
"Handler function for (:info ...) sexps received from the server
process."
(let ((type (plist-get info :info)))
(cond
;; (:info :version "3.1")
((eq type 'add)
;; update our path=>docid map; we use this when composing messages to
;; add draft messages to the db, so when we're sending them, we can move
;; to the sent folder using the `mm/proc-move'.
(puthash (plist-get info :path) (plist-get info :docid) mm/path-docid-map))
((eq type 'version)
(setq
mm/version (plist-get info :version)
mm/doccount (plist-get-info :doccount)))
((eq type 'index)
(if (eq (plist-get info :status) 'running)
(message (format "Indexing... processed %d, updated %d"
(plist-get info :processed) (plist-get info :updated)))
(message
(format "Indexing completed; processed %d, updated %d, cleaned-up %d"
(plist-get info :processed) (plist-get info :updated)
(plist-get info :cleaned-up)))))
((plist-get info :message) (message "%s" (plist-get info :message))))))
(defun mm/default-handler (&rest args)
"Dummy handler function."
(error "Not handled: %S" args))
(defconst mm/server-name "*mm-server"
"*internal* Name of the server process, buffer.")
(defun mm/start-proc ()
"Start the mu server process."
;; TODO: add version check
(unless (file-executable-p mm/mu-binary)
(error (format "%S not found" mm/mu-binary)))
(let* ((process-connection-type nil) ;; use a pipe
(args '("server"))
(args (append args (when mm/mu-home
(list (concat "--muhome=" mm/mu-home))))))
(setq mm/buf "")
(setq mm/mu-proc (apply 'start-process mm/server-name mm/server-name
mm/mu-binary args))
;; register a function for (:info ...) sexps
(setq mm/proc-info-func 'mm/proc-info-handler)
(when mm/mu-proc
(set-process-coding-system mm/mu-proc 'binary 'utf-8-unix)
(set-process-filter mm/mu-proc 'mm/proc-filter)
(set-process-sentinel mm/mu-proc 'mm/proc-sentinel))))
(defun mm/kill-proc ()
"Kill the mu server process."
(let* ((buf (get-buffer mm/server-name))
(proc (and buf (get-buffer-process buf))))
(when proc
(let ((delete-exited-processes t))
;; the mu server signal handler will make it quit after 'quit'
(mm/proc-send-command "quit"))
;; try sending SIGINT (C-c) to process, so it can exit gracefully
(ignore-errors
(signal-process proc 'SIGINT))))
(setq
mm/mu-proc nil
mm/buf nil))
(defun mm/proc-is-running ()
(and mm/mu-proc (eq (process-status mm/mu-proc) 'run)))
(defun mm/proc-eat-sexp-from-buf ()
"'Eat' the next s-expression from `mm/buf'. `mm/buf gets its
contents from the mu-servers in the following form:
\376<len-of-sexp>\376<sexp>
Function returns this sexp, or nil if there was none. `mm/buf' is
updated as well, with all processed sexp data removed."
(when mm/buf
;; TODO: maybe try a non-regexp solution?
(let* ((b (string-match "\376\\([0-9]+\\)\376" mm/buf))
(sexp-len
(when b (string-to-number (match-string 1 mm/buf)))))
;; does mm/buf contain the full sexp?
(when (and b (>= (length mm/buf) (+ sexp-len (match-end 0))))
;; clear-up start
(setq mm/buf (substring mm/buf (match-end 0)))
;; note: we read the input in binary mode -- here, we take the part that
;; is the sexp, and convert that to utf-8, before we interpret it.
(let ((objcons
(ignore-errors ;; note: this may fail if we killed the process
;; in the middle
(read-from-string
(decode-coding-string (substring mm/buf 0 sexp-len) 'utf-8)))))
(when objcons
(setq mm/buf (substring mm/buf sexp-len))
(car objcons)))))))
(defun mm/proc-filter (proc str)
"A process-filter for the 'mu server' output; it accumulates the
strings into valid sexps by checking of the ';;eox' end-of-sexp
marker, and then evaluating them.
The server output is as follows:
1. an error
(:error 2 :error-message \"unknown command\")
;; eox
=> this will be passed to `mm/proc-error-func'.
2a. a message sexp looks something like:
\(
:docid 1585
: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\"
:maildir: \"/archive\"
: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>\"
\)
;; eox
=> this will be passed to `mm/proc-header-func'.
2b. After the list of message sexps has been returned (see 2a.),
we'll receive a sexp that looks like
(:found <n>) with n the number of messages found. The <n> will be
passed to `mm/proc-found-func'.
3. a view looks like:
(:view <msg-sexp>)
=> the <msg-sexp> (see 2.) will be passed to `mm/proc-view-func'.
4. a database update looks like:
(:update <msg-sexp> :move <nil-or-t>)
=> the <msg-sexp> (see 2.) will be passed to
`mm/proc-update-func', :move tells us whether this is a move to
another maildir, or merely a flag change.
5. a remove looks like:
(:remove <docid>)
=> the docid will be passed to `mm/proc-remove-func'
6. a compose looks like:
(:compose <msg-sexp> :action <reply|forward>) => the <msg-sexp>
and either 'reply or 'forward will be passed
`mm/proc-compose-func'."
(mm/proc-log "* Received %d byte(s)" (length str))
(setq mm/buf (concat mm/buf str)) ;; update our buffer
(let ((sexp (mm/proc-eat-sexp-from-buf)))
(while sexp
(mm/proc-log "<- %S" sexp)
(cond
;; a header plist can be recognized by the existence of a :date field
((plist-get sexp :date)
(funcall mm/proc-header-func sexp))
;; the found sexp, we receive after getting all the headers
((plist-get sexp :found)
(funcall mm/proc-found-func (plist-get sexp :found)))
;; viewin a specific message
((plist-get sexp :view)
(funcall mm/proc-view-func (plist-get sexp :view)))
;; receive a pong message
((plist-get sexp :pong)
(funcall mm/proc-pong-func
(plist-get sexp :version) (plist-get sexp :doccount)))
;; something got moved/flags changed
((plist-get sexp :update)
(funcall mm/proc-update-func
(plist-get sexp :update) (plist-get sexp :move)))
;; a message got removed
((plist-get sexp :remove)
(funcall mm/proc-remove-func (plist-get sexp :remove)))
;; start composing a new message
((plist-get sexp :compose)
(funcall mm/proc-compose-func
(plist-get sexp :compose-type)
(plist-get sexp :compose)))
;; get some info
((plist-get sexp :info)
(funcall mm/proc-info-func sexp))
;; receive an error
((plist-get sexp :error)
(funcall mm/proc-error-func sexp))
(t (message "Unexpected data from server [%S]" sexp)))
(setq sexp (mm/proc-eat-sexp-from-buf)))))
(defun mm/proc-sentinel (proc msg)
"Function that will be called when the mu-server process
terminates."
(let ((status (process-status proc)) (code (process-exit-status proc)))
(setq mm/mu-proc nil)
(setq mm/buf "") ;; clear any half-received sexps
(cond
((eq status 'signal)
(cond
((eq code 9) (message nil))
;;(message "the mu server process has been stopped"))
(t (message (format "mu server process received signal %d" code)))))
((eq status 'exit)
(cond
((eq code 0)
(message nil)) ;; don't do anything
((eq code 11)
(message "Database is locked by another process"))
((eq code 19)
(message "Database is empty; try indexing some messages"))
(t (message (format "mu server process ended with exit code %d" code)))))
(t
(message "something bad happened to the mu server process")))))
(defconst mm/proc-log-buffer-name "*mm-log*"
"*internal* Name of the logging buffer.")
(defun mm/proc-log (frm &rest args)
"Write something in the *mm-log* buffer - mainly useful for debugging."
(when mm/debug
(with-current-buffer (get-buffer-create mm/proc-log-buffer-name)
(goto-char (point-max))
(insert (apply 'format (concat (format-time-string "%Y-%m-%d %T "
(current-time)) frm "\n") args)))))
(defun mm/proc-send-command (frm &rest args)
"Send as command to the mu server process; start the process if needed."
(unless (mm/proc-is-running)
(mm/start-proc))
(let ((cmd (apply 'format frm args)))
(mm/proc-log (concat "-> " cmd))
(process-send-string mm/mu-proc (concat cmd "\n"))))
(defun mm/proc-remove-msg (docid)
"Remove message identified by DOCID. The results are reporter
through either (:update ... ) or (:error ) sexp, which are handled
my `mm/proc-update-func' and `mm/proc-error-func', respectively."
(mm/proc-send-command "remove %d" docid))
(defun mm/proc-find (expr &optional maxnum)
"Start a database query for EXPR, getting up to MAXNUM
results (or -1 for unlimited). For each result found, a function is
called, depending on the kind of result. The variables
`mm/proc-header-func' and `mm/proc-error-func' contain the function
that will be called for, resp., a message (header row) or an
error."
(mm/proc-send-command "find \"%s\" %d"
expr (if maxnum maxnum -1)))
(defun mm/proc-move-msg (docid targetmdir &optional flags)
"Move message identified by DOCID to TARGETMDIR, optionally
setting FLAGS in the process.
TARGETDIR must be a maildir, that is, the part _without_ cur/ or
new/ or the root-maildir-prefix. E.g. \"/archive\". This directory
must already exist.
The FLAGS parameter can have the following forms:
1. a list of flags such as '(passed replied seen)
2. a string containing the one-char versions of the flags, e.g. \"PRS\"
3. a delta-string specifying the changes with +/- and the one-char flags,
e.g. \"+S-N\" to set Seen and remove New.
The flags are any of `deleted', `flagged', `new', `passed', `replied' `seen' or
`trashed', or the corresponding \"DFNPRST\" as defined in [1]. See
`mm/string-to-flags' and `mm/flags-to-string'.
The server reports the results for the operation through
`mm/proc-update-func'.
The results are reported through either (:update ... )
or (:error ) sexp, which are handled my `mm/proc-update-func' and
`mm/proc-error-func', respectively."
(let
((flagstr (if (stringp flags) flags (mm/flags-to-string flags)))
(fullpath (concat mm/maildir targetmdir)))
(unless (and (file-directory-p fullpath) (file-writable-p fullpath))
(error "Not a writable directory: %s" fullpath))
;; note, we send the maildir, *not* the full path
(mm/proc-send-command "move %d \"%s\" %s" docid
targetmdir flagstr)))
(defun mm/proc-flag (docid-or-msgid flags)
"Set FLAGS for the message identified by either DOCID-OR-MSGID."
(let ((flagstr (if (stringp flags) flags (mm/flags-to-string flags))))
(mm/proc-send-command "flag %S %s" docid-or-msgid flagstr)))
(defun mm/proc-index (maildir)
"Update the message database for MAILDIR."
(mm/proc-send-command "index \"%s\"" maildir))
(defun mm/proc-add (path maildir)
"Add the message at PATH to the database, with MAILDIR
set to e.g. '/drafts'; if this works, we will receive (:info :path
<path> :docid <docid>)."
(mm/proc-send-command "add \"%s\" \"%s\"" path maildir))
(defun mm/proc-save (docid partidx path)
"Save attachment PARTIDX from message with DOCID to PATH."
(mm/proc-send-command "save %d %d \"%s\"" docid partidx path))
(defun mm/proc-open (docid partidx)
"Open attachment PARTIDX from message with DOCID."
(mm/proc-send-command "open %d %d" docid partidx))
(defun mm/proc-ping ()
"Sends a ping to the mu server, expecting a (:pong ...) in
response."
(mm/proc-send-command "ping"))
(defun mm/proc-view-msg (docid)
"Get one particular message based on its DOCID. The result will
be delivered to the function registered as `mm/proc-message-func'."
(mm/proc-send-command "view %d" docid))
(defun mm/proc-compose (compose-type docid)
"Start composing a message with DOCID and COMPOSE-TYPE (a symbol,
either `forward', `reply' or `edit'.
The result will be delivered to the function registered as
`mm/proc-compose-func'."
(unless (member compose-type '(forward reply edit))
(error "Unsupported compose-type"))
(mm/proc-send-command "compose %s %d" (symbol-name compose-type) docid))
(defconst mm/update-buffer-name "*update*"
"*internal* Name of the buffer to download mail")
(defun mm/proc-retrieve-mail-update-db ()
"Try to retrieve mail (using the user-provided shell command),
and update the database afterwards."
(unless mm/get-mail-command
(error "`mm/get-mail-command' is not defined"))
(let ((buf (get-buffer-create mm/update-buffer-name)))
(split-window-vertically -8)
(switch-to-buffer-other-window buf)
(with-current-buffer buf
(erase-buffer))
(message "Retrieving mail...")
(call-process mm/get-mail-command nil buf t)
(message "Updating the database...")
(mm/proc-index mm/maildir)
(with-current-buffer buf
(kill-buffer-and-window))))
(provide 'mm-proc)

420
emacs/mm-send.el Normal file
View File

@ -0,0 +1,420 @@
;; mm-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
;; gnus' message mode
;; mm
;;; Code:
(eval-when-compile (require 'cl))
;; we use some stuff from gnus...
(require 'message)
(require 'mail-parse)
;; internal variables / constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defconst mm/msg-draft-name "*mm-draft*"
"Name for draft messages.")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FIXME
(defun mm/mu-binary-version () "0.98pre")
(defun mm/msg-user-agent ()
"Return the User-Agent string for mm. This is either the value
of `mm/user-agent', or, if not set, a string based on the
version of mm and emacs."
(or mm/user-agent
(format "mu %s; emacs %s" (mm/mu-binary-version) emacs-version)))
(defun mm/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"))
(defun mm/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 `mm/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)
(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)))))
(defun mm/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 mm/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\."
(when lst
(mapconcat
(lambda (recip)
(let ((name (car recip)) (email (cdr recip)))
(if name
(format "%s <%s>" name email)
(format "%s" email)))) lst ", ")))
(defun mm/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)))
(defun mm/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))
(old-msgid (plist-get msg :message-id)))
(when old-msgid
(setq refs (append refs (list old-msgid)))
(mapconcat
(lambda (msgid) (format "<%s>" msgid))
refs ","))))
(defun mm/msg-to-create (msg)
"Construct the To: header for a reply-message based on some
message MSG. This takes the Reply-To address of MSG if it exist, or
the From:-address otherwise. The result is either nil or a string
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)
(mm/msg-recipients-to-string (plist-get msg :from))))
(defun mm/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 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
(setq cc-lst (mm/msg-recipients-remove cc-lst user-mail-address))
(mm/msg-recipients-to-string cc-lst)))
(defun mm/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 mm/msg-create-reply (msg)
"Create a draft message as a reply to MSG.
A reply message has fields:
From: - see `mu-msg-from-create'
To: - see `mm/msg-to-create'
Cc: - see `mm/msg-cc-create'
Subject: - `mm/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 `mm/msg-references-create'
In-Reply-To: - message-id of MSG
User-Agent - see `mm/msg-user-agent'
Then follows `mail-header-separator' (for `message-mode' to separate
body from headers)
And finally, the cited body of MSG, as per `mm/msg-cite-original'."
(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))))))
(concat
(mm/msg-header "From" (or (mm/msg-from-create) ""))
(when (boundp 'mail-reply-to)
(mm/msg-header "Reply-To" mail-reply-to))
(mm/msg-header "To" (or (mm/msg-to-create msg) ""))
(mm/msg-header "Cc" (mm/msg-cc-create msg reply-all))
(mm/msg-header "User-agent" (mm/msg-user-agent))
(mm/msg-header "References" (mm/msg-references-create msg))
(mm/msg-header "In-reply-to" (format "<%s>" (plist-get msg :message-id)))
(mm/msg-header "Subject"
(concat mm/msg-reply-prefix (plist-get msg :subject)))
(propertize mail-header-separator 'read-only t 'intangible t) '"\n"
"\n\n"
(mm/msg-cite-original msg))))
;; TODO: attachments
(defun mm/msg-create-forward (msg)
"Create a draft forward message for MSG.
A forward message has fields:
From: - see `mm/msg-from-create'
To: - empty
Subject: - `mm/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 `mm/msg-references-create'
User-Agent - see `mm/msg-user-agent'
Then follows `mail-header-separator' (for `message-mode' to separate
body from headers)
And finally, the cited body of MSG, as per `mm/msg-cite-original'."
(concat
(mm/msg-header "From" (or (mm/msg-from-create) ""))
(when (boundp 'mail-reply-to)
(mm/msg-header "Reply-To" mail-reply-to))
(mm/msg-header "To" "")
(mm/msg-header "User-agent" (mm/msg-user-agent))
(mm/msg-header "References" (mm/msg-references-create msg))
(mm/msg-header"Subject"
(concat mm/msg-forward-prefix (plist-get msg :subject)))
(propertize mail-header-separator 'read-only t 'intangible t) "\n"
"\n\n"
(mm/msg-cite-original msg)))
(defun mm/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 `mm/msg-user-agent'
Then follows `mail-header-separator' (for `message-mode' to separate
body from headers)."
(concat
(mm/msg-header "From" (or (mm/msg-from-create) ""))
(when (boundp 'mail-reply-to)
(mm/msg-header "Reply-To" mail-reply-to))
(mm/msg-header "To" "")
(mm/msg-header "User-agent" (mm/msg-user-agent))
(mm/msg-header "Subject" "")
(propertize mail-header-separator 'read-only t 'intangible t) "\n"))
(defun mm/msg-open-draft (compose-type &optional msg)
"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
the database, `mm/path-docid-map' will be updated, so that we can
use the new docid. Returns the full path to the new message."
(let* ((hostname
(downcase
(save-match-data
(substring system-name
(string-match "^[^.]+" system-name) (match-end 0)))))
(draft
(concat mm/maildir mm/drafts-folder "/cur/"
(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
(reply (mm/msg-create-reply msg))
(forward (mm/msg-create-forward msg))
(new (mm/msg-create-new))
(t (error "unsupported compose-type %S" compose-type)))))
(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
;; the new docid from `mm/path-docid-map'.
(mm/proc-add draft mm/drafts-folder)
draft))
(defun mm/send-compose-handler (compose-type &optional msg)
"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
of `mm/maildir' and `mm/drafts-folder' (therefore, these must be
set).
The message file name is a unique name determined by
`mm/msg-draft-file-name'.
The initial STR would be created from either `mm/msg-create-reply',
ar`mm/msg-create-forward' or `mm/msg-create-new'. The editing buffer is
using Gnus' `message-mode'."
(unless mm/maildir (error "mm/maildir not set"))
(unless mm/drafts-folder (error "mm/drafts-folder not set"))
(let ((draft
(if (member compose-type '(reply forward new))
(mm/msg-open-draft compose-type msg)
(if (eq compose-type 'edit)
(plist-get msg :path)
(error "unsupported compose-type %S" compose-type)))))
(unless (file-readable-p draft)
(error "Cannot read %s" path))
(find-file draft)
(message-mode)
(make-local-variable 'write-file-functions)
;; update the db when the file is saved...]
(add-to-list 'write-file-functions
(lambda() (mm/proc-add (buffer-file-name) mm/drafts-folder)))
;; hook our functions up with sending of the message
(add-hook 'message-sent-hook 'mm/msg-save-to-sent nil t)
(add-hook 'message-sent-hook 'mm/send-set-parent-flag nil t)
(let ((message-hidden-headers
`("^References:" "^Face:" "^X-Face:" "^X-Draft-From:"
"^User-agent:")))
(message-hide-headers))
(if (eq compose-type 'new)
(message-goto-to)
(message-goto-body))))
(defun mm/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'."
(unless mm/sent-folder (error "mm/sent-folder not set"))
(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)
(let ((docid (gethash (buffer-file-name) mm/path-docid-map)))
(unless docid (error "unknown message (%S)" (buffer-file-name)))
;; ok, all seems well, well move the message to the sent-folder
(mm/proc-move-msg docid mm/sent-folder "-T-D+S")
;; we can remove the value from the hash now, if we can establish there
;; are not other compose buffers using this very same docid...
;; mark the buffer as read-only, as its pointing at a non-existing file
;; now...
(kill-buffer-and-window)
(message "Message has been sent"))))
(defun mm/send-set-parent-flag ()
"Set the 'replied' flag on messages we replied to, and the
'passed' flag on message we have forwarded.
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.
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'."
(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))
(setq forwarded-from (car-safe (last refs)))))))
;; remove the <>
(when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
(mm/proc-flag (match-string 1 in-reply-to) "+R"))
(when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
(mm/proc-flag (match-string 1 forwarded-from) "+P"))))
(provide 'mm-send)

119
emacs/mm-tech.org Normal file
View File

@ -0,0 +1,119 @@
* mm
I haven't written many =emacs-fu= posts recently, but that doesn't mean I
haven't used emacs a lot. In fact, over the last few months I've been working on
a bigger emacs-related project; the working title is =mm=, and it's an
emacs-based e-mail client based on my [[http://www.djcbsoftware.nl/code/mu][mu]] maildir searcher/indexer that I
discussed before. Even though I've been using =mm= myself for about two months,
it's not really ready from prime-time yet - but I'm planning to have something
ready this year still.
In this post, let me discuss some of the technical background, which may be
interesting for others planning emacs-based front-ends to other tools.
* How to make an emacs-based e-mail client
Emacs does not (as of today) support threads; but one way to do asynchronous
processing is to start another process, and let emacs deal with its
output. Let us see how...
** Getting output from =mu=
One way to implement this (for =mu=), is to call the =mu= command-line tool
with some parameters and then parse its output. In fact, that is how some
tools do it, and it was my first approach - so I would invoke =mu find= and
then process the output in emacs (more about that in a minute).
However, then I realized that I'd need to load the entire e-mail Xapian
database for each invocation. Wouldn't it be nicer to keep a running =mu=
instance around? Indeed, it would - so I implemented the =mu server=
sub-command. Now, when you run =mu server=, you get a shell, in which you can
give commands to =mu=, and which will then spit out the results. =mu server=
is not really meant for humans, but still I can use it manually, which is
great for debugging.
The next question was what format mu should use for its output for emacs to
process. Some other programs use =JSON= here, but I figured that it would be
easier (and possibly, more efficient) just to use emacs' native
=s-expressions= (=plists= to be precise). So that is what I did - and I can
easily evaluate them using =read-from-string=.
** Processing the output in emacs
So, now let's look how we process the data from =mu server= in emacs.
First you create a process with, for example, =start-process=, and then
register a filter function for it, which will be invoked whenever the process
has some chunk of output. Something like:
#+BEGIN_SRC emacs-lisp
(let ((proc (start-process <arguments>)))
(set-process-filter proc 'my-process-filter)
(set-process-sentinel proc 'my-process-sentinel))
#+END_SRC
Note, the process sentinel is invoked when the process is terminated -- so there
you can clean things up.
The function =my-process-filter= is a user-defined function that takes the
process and the chunk of output as arguments; in =mm= it looks something like
(pseudo-lisp):
#+begin_SRC emacs-lisp
(defun my-process-filter (proc str)
(setq mm/buf (concat mm/buf str)) ;; a global var updated with the new chunk
(when <we-have-received-a-full-expression>
<eat-expression-from mm/buf>
<evaluate-expression>))
#+end_src
The =<evaluate-expression>= de-multiplexes the s-expression we got. For example,
if the s-expression looks like an e-mail message header, it will be processed by
the header-handling function, which will append it to the header list. If the
s-expression looks like an error message, it will be reported to the user. And
so on.
Finally, let me try to answer some anticipated questions:
* Why does the world need yet another e-mail client?
I don't the world needs another client, but I spend a *lot* of time
(professionally and privately) with my e-mail client, so I'd like it to behave
exactly like I want it to. An even more important goal for me was to write
some bigger program in emacs lisp, to better understand the language and its
idioms.
Specifically, when it comes to emacs-based clients, I have tried a few of
them. I never really got into =gnus=; I think it is by far the most popular
emacs-based mail client, but I found it hard to make behave the way I like it;
and in particular, I do not like its indirect approach to Maildirs.
Then, for some years I've been using Wanderlust; a fine, very feature-rich
client, but it shows its age - and especially with emacs-24, its cache file
got corrupted very often, requiring me to delete them etc. Still, you will
recognize some Wanderlust features in =mm/mu=.
* Why not use [[http://notmuchmail.org/][notmuch]]? It seems similar.
There are certainly similarities with =notmuch= (and to some lesser extent,
with [[https://github.com/nicferrier/md][md]]) -- the overall architecture is similar: both are scanning maildirs,
using [[http://spruce.sourceforge.net/gmime/][GMime]] and [[http://xapian.org/][Xapian]]. (=mu= precedes =notmuch= by a year or so; but
=notmuch= was the first to add an emacs front-end).
There are some differences as well. The main thing is that in =notmuch='s
philosophy, messages are usually not moved or deleted, but instead uses tags
in the database. While tags are nice, I like the 'state' to be in the messages
and the folders they are in, which it easy to synchronize with other email
clients (or synchronize with IMAP-folders through [[http://offlineimap.org/][OfflineIMAP]]). I'd like to be
able to move messages around, delete messages and so on. This is in fact one
of the things I liked in [[http://www.gohome.org/wl/][Wanderlust]], and wouldn't want to live without - so
=mu=/=mm= make this really easy.
Clearly, the emacs-interface to =notmuch= is more mature, and the development
team is bigger, so I'd give it a try. On the other hand, if you happen to like
e-mail the way I like it, =mm= may be something for you.

672
emacs/mm-view.el Normal file
View File

@ -0,0 +1,672 @@
;; mm-view.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 are function related to creating the list of one-line
;; descriptions of emails, aka 'headers' (not to be confused with headers like
;; 'To:' or 'Subject:')
;; mm
;;; Code:
(eval-when-compile (require 'cl))
(require 'html2text)
(require 'filladapt)
(require 'comint)
(defconst mm/view-buffer-name "*mm-view*"
"*internal* Name for the message view buffer")
(defconst mm/view-raw-buffer-name "*mm-view-raw*"
"*internal* Name for the raw message view buffer")
;; some buffer-local variables
(defvar mm/hdrs-buffer nil
"*internal* Headers buffer connected to this view.")
(defvar mm/current-msg nil
"*internal* The plist describing the current message.")
(defun mm/view (msg hdrsbuf &optional update)
"Display the message MSG in a new buffer, and keep in sync with HDRSBUF.
'In sync' here means that moving to the next/previous message in
the the message view affects HDRSBUF, as does marking etc. If
UPDATE is non-nil, the current message will be (visually) updated.
As a side-effect, a message that is being viewed loses its 'unread'
marking if it still had that."
(let ((buf (get-buffer-create mm/view-buffer-name))
(inhibit-read-only t))
(with-current-buffer buf
(erase-buffer)
(insert
(mapconcat
(lambda (field)
(let ((fieldname (cdr (assoc field mm/header-names)))
(fieldval (plist-get msg field)))
(case field
(:subject (mm/view-header fieldname fieldval))
(:path (mm/view-header fieldname fieldval))
(:maildir (mm/view-header fieldname fieldval))
(:flags (mm/view-header fieldname
(if fieldval (format "%S" fieldval) "")))
;; contact fields
(:to (mm/view-contacts msg field))
(:from (mm/view-contacts msg field))
(:cc (mm/view-contacts msg field))
(:bcc (mm/view-contacts msg field))
;; if we (`user-mail-address' are the From, show To, otherwise,
;; show From
(:from-or-to
(let* ((from (plist-get msg :from))
(from (and from (cdar from))))
(if (and from (string-match mm/user-mail-address-regexp from))
(mm/view-contacts msg :to)
(mm/view-contacts msg :from))))
;; date
(:date
(let ((datestr
(when fieldval (format-time-string mm/view-date-format fieldval))))
(if datestr (mm/view-header fieldname datestr) "")))
;; size
(:size (mm/view-size msg)
(let ((sizestr (when size (format "%d bytes"))))
(if sizestr (mm/view-header fieldname sizestr))))
;; attachments
(:attachments (mm/view-attachments msg))
(t (error "Unsupported field: %S" field)))))
mm/view-fields "")
"\n"
(mm/view-body msg))
;; initialize view-mode
(mm/view-mode)
(setq ;; these are buffer-local
mode-name (if (plist-get msg :subject)
(truncate-string-to-width (plist-get msg :subject) 16 0 nil t)
(propertize "No subject" 'face 'mm/system-face))
mm/current-msg msg
mm/hdrs-buffer hdrsbuf
mm/link-map (make-hash-table :size 32 :rehash-size 2 :weakness nil))
(switch-to-buffer buf)
(goto-char (point-min))
(mm/view-beautify)
(unless update
(mm/view-mark-as-read-maybe)))))
(defun mm/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"))
(defun mm/view-header (key val &optional dont-propertize-val)
"Show header FIELD for MSG with KEY. ie. <KEY>: value-of-FIELD."
(if val
(concat
(propertize key 'face 'mm/view-header-key-face) ": "
(if dont-propertize-val
val
(propertize val 'face 'mm/view-header-value-face))
"\n")
""))
(defun mm/view-contacts (msg field)
"Add a header for a contact field (ie., :to, :from, :cc, :bcc)."
(let* ((lst (plist-get msg field))
(fieldname (cdr (assoc field mm/header-names)))
(contacts
(and lst
(mapconcat
(lambda(c)
(let ((name (car c)) (email (cdr c)))
(if name
(format "%s <%s>" name email)
(format "%s" email)))) lst ", "))))
(if contacts
(mm/view-header fieldname contacts)
"")))
(defvar mm/attach-map nil
"*internal* Hash which maps a number to a (part-id name mime-type).")
(defun mm/view-attachments (msg)
"Display attachment information; the field looks like something like:
:attachments ((:index 4 :name \"test123.doc\"
:mime-type \"application/msword\" :size 1234))."
(let ((atts (plist-get msg :attachments)))
(when atts
(setq mm/attach-map
(make-hash-table :size 32 :rehash-size 2 :weakness nil))
(let* ((id 0)
(vals
(mapconcat
(lambda (att)
(let ( (index (plist-get att :index))
(name (plist-get att :name))
(mime-type (plist-get att :mime-type))
(size (plist-get att :size)))
(incf id)
(puthash id att mm/attach-map)
(concat
(propertize (format "[%d]" id) 'face 'mm/view-attach-number-face)
(propertize name 'face 'mm/view-link-face)
(if size
(concat
"(" (propertize (mm/display-size size) 'face 'mm/view-header-key-face)
")")
"")
)))
atts ", ")))
(mm/view-header (format "Attachments(%d)" id) vals t)))))
(defvar mm/view-mode-map nil
"Keymap for \"*mm-view*\" buffers.")
(unless mm/view-mode-map
(setq mm/view-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" 'mm/view-quit-buffer)
(define-key map "s" 'mm/search)
(define-key map "S" 'mm/search-full)
(define-key map "b" 'mm/search-bookmark)
(define-key map "j" 'mm/jump-to-maildir)
(define-key map "g" 'mm/view-go-to-url)
(define-key map "f" 'mm/compose-forward)
(define-key map "r" 'mm/compose-reply)
(define-key map "c" 'mm/compose-new)
(define-key map "e" 'mm/edit-draft)
(define-key map "." 'mm/view-raw)
(define-key map "|" 'mm/view-pipe)
;; (define-key map "I" 'mm/inspect-message)
;; intra-message navigation
(define-key map (kbd "SPC") 'scroll-up)
(define-key map (kbd "<home>")
'(lambda () (interactive) (goto-char (point-min))))
(define-key map (kbd "<end>")
'(lambda () (interactive) (goto-char (point-max))))
(define-key map (kbd "RET")
'(lambda () (interactive) (scroll-up 1)))
(define-key map (kbd "<backspace>")
'(lambda () (interactive) (scroll-up -1)))
;; navigation between messages
(define-key map "n" 'mm/view-next-header)
(define-key map "p" 'mm/view-prev-header)
;; attachments
(define-key map "e" 'mm/view-extract-attachment)
(define-key map "o" 'mm/view-open-attachment)
;; marking/unmarking
(define-key map (kbd "<backspace>") 'mm/mark-for-trash)
(define-key map "d" 'mm/view-mark-for-trash)
(define-key map (kbd "<delete>") 'mm/view-mark-for-delete)
(define-key map "D" 'mm/view-mark-for-delete)
(define-key map "a" 'mm/mark-for-move-quick)
(define-key map "m" 'mm/view-mark-for-move)
;; misc
(define-key map "w" 'mm/view-toggle-wrap-lines)
(define-key map "h" 'mm/view-toggle-hide-cited)
(define-key map "R" 'mm/view-refresh)
;; next 3 only warn user when attempt in the message view
(define-key map "u" 'mm/view-unmark)
(define-key map "U" 'mm/view-unmark)
(define-key map "x" 'mm/view-marked-execute)
;; menu
(define-key map [menu-bar] (make-sparse-keymap))
(let ((menumap (make-sparse-keymap "View")))
(define-key map [menu-bar headers] (cons "View" menumap))
(define-key menumap [quit-buffer] '("Quit view" . mm/view-quit-buffer))
(define-key menumap [sepa0] '("--"))
(define-key menumap [wrap-lines]
'("Toggle wrap lines" . mm/view-toggle-wrap-lines))
(define-key menumap [hide-cited]
'("Toggle hide cited" . mm/view-toggle-hide-cited))
(define-key menumap [view-raw]
'("View raw message" . mm/view-raw))
(define-key menumap [pipe]
'("Pipe through shell" . mm/view-pipe))
(define-key menumap [inspect]
'("Inspect with guile" . mm/inspect-message))
(define-key menumap [sepa8] '("--"))
(define-key menumap [open-att]
'("Open attachment" . mm/view-open-attachment))
(define-key menumap [extract-att]
'("Extract attachment" . mm/view-extract-attachment))
(define-key menumap [goto-url]
'("Visit URL" . mm/view-go-to-url))
(define-key menumap [sepa1] '("--"))
(define-key menumap [mark-delete]
'("Mark for deletion" . mm/view-mark-for-delete))
(define-key menumap [mark-trash]
'("Mark for trash" . mm/view-mark-for-trash))
(define-key menumap [mark-move]
'("Mark for move" . mm/view-mark-for-move))
(define-key menumap [sepa2] '("--"))
(define-key menumap [compose-new] '("Compose new" . mm/compose-new))
(define-key menumap [forward] '("Forward" . mm/compose-forward))
(define-key menumap [reply] '("Reply" . mm/compose-reply))
(define-key menumap [sepa3] '("--"))
(define-key menumap [search] '("Search" . mm/search))
(define-key menumap [jump] '("Jump to maildir" . mm/jump-to-maildir))
(define-key menumap [sepa4] '("--"))
(define-key menumap [next] '("Next" . mm/view-next-header))
(define-key menumap [previous] '("Previous" . mm/view-prev-header)))
map)))
(fset 'mm/view-mode-map mm/view-mode-map)
(defvar mm/wrap-lines nil
"*internal* Whether to wrap lines or not (variable controlled by
`mm/view-toggle-wrap-lines').")
(defvar mm/hide-cited nil
"*internal* Whether to hide cited lines or not (the variable can
be changed with `mm/view-toggle-hide-cited').")
(defun mm/view-mode ()
"Major mode for viewing an e-mail message."
(interactive)
(kill-all-local-variables)
(use-local-map mm/view-mode-map)
(make-local-variable 'mm/hdrs-buffer)
(make-local-variable 'mm/current-msg)
(make-local-variable 'mm/link-map)
(make-local-variable 'mm/wrap-lines)
(make-local-variable 'mm/hide-cited)
(setq major-mode 'mm/view-mode mode-name mm/view-buffer-name)
(setq truncate-lines t buffer-read-only t))
;;;;;;
;; we mark messages are as read when we leave the message; ie., when skipping to
;; the next/previous one, or leaving the view buffer altogether.
(defun mm/view-mark-as-read-maybe ()
"Clear the current message's New/Unread status and set it to
Seen; if the message is not New/Unread, do nothing."
(when mm/current-msg
(let ((flags (plist-get mm/current-msg :flags))
(docid (plist-get mm/current-msg :docid)))
;; is it a new message?
(when (or (member 'unread flags) (member 'new flags))
(mm/proc-flag docid "+S-u-N")))))
(defvar mm/link-map nil
"*internal* A map of some number->url so we can jump to url by number.")
(defun mm/view-beautify ()
"Improve the message view a bit, by making URLs clickable,
removing '^M' etc."
(let ((num 0))
(save-excursion
;; remove the stupid CRs
(goto-char (point-min))
(while (re-search-forward "[\r\240]" nil t)
(replace-match " " nil t))
;; give the footer a different color...
(goto-char (point-min))
(let ((p (search-forward "\n-- \n" nil t)))
(when p
(add-text-properties p (point-max) '(face mm/view-footer-face))))
;; this is fairly simplistic...
(goto-char (point-min))
(while (re-search-forward "\\(https?://[-a-zA-Z0-9?_.$%/=+&#@!~,:;]*\\)\\>"
nil t)
(let ((subst (propertize (match-string-no-properties 0)
'face 'mm/view-link-face)))
(incf num)
(puthash num (match-string-no-properties 0) mm/link-map)
(replace-match (concat subst
(propertize (format "[%d]" num)
'face 'mm/view-url-number-face))))))))
;; raw mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; some buffer-local variables
(defvar mm/view-buffer nil
"*internal* View buffer connected to this raw view.")
(defun mm/view-raw-mode ()
"Major mode for viewing of raw e-mail message."
(interactive)
(kill-all-local-variables)
(use-local-map mm/view-raw-mode-map)
(make-local-variable 'mm/view-buffer)
(setq major-mode 'mm/view-raw-mode
mode-name "mm: raw view")
(setq truncate-lines t buffer-read-only t))
(defvar mm/view-raw-mode-map nil
"Keymap for \"*mm-view-raw*\" buffers.")
(unless mm/view-raw-mode-map
(setq mm/view-raw-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "q" 'mm/view-raw-quit-buffer)
(define-key map "." 'mm/view-raw-quit-buffer)
;; intra-message navigation
(define-key map (kbd "SPC") 'scroll-up)
(define-key map (kbd "<home>")
'(lambda () (interactive) (goto-char (point-min))))
(define-key map (kbd "<end>")
'(lambda () (interactive) (goto-char (point-max))))
(define-key map (kbd "RET")
'(lambda () (interactive) (scroll-up 1)))
(define-key map (kbd "<backspace>")
'(lambda () (interactive) (scroll-up -1)))
;; menu
(define-key map [menu-bar] (make-sparse-keymap))
(let ((menumap (make-sparse-keymap "Raw view")))
(define-key map [menu-bar headers] (cons "Raw view" menumap))
(define-key menumap [quit-buffer] '("Quit" .
mm/view-raw-quit-buffer))
map))))
(fset 'mm/view-raw-mode-map mm/view-raw-mode-map)
(defun mm/view-raw-message (msg view-buffer)
"Display the raw contents of message MSG in a new buffer."
(let ((buf (get-buffer-create mm/view-raw-buffer-name))
(inhibit-read-only t)
(file (plist-get msg :path)))
(unless (and file (file-readable-p file))
(error "Not a readable file: %S" file))
(with-current-buffer buf
(erase-buffer)
(insert-file file)
;; initialize view-mode
(mm/view-raw-mode)
(setq mm/view-buffer view-buffer)
(switch-to-buffer buf)
(goto-char (point-min)))))
(defun mm/view-shell-command-on-raw-message (msg view-buffer cmd)
"Process the raw message with shell command CMD."
(let ((buf (get-buffer-create mm/view-raw-buffer-name))
(inhibit-read-only t)
(file (plist-get msg :path)))
(unless (and file (file-readable-p file))
(error "Not a readable file: %S" file))
(with-current-buffer buf
(erase-buffer)
(process-file-shell-command cmd file buf)
(mm/view-raw-mode)
(setq mm/view-buffer view-buffer)
(switch-to-buffer buf)
(goto-char (point-min)))))
(defun mm/view-raw-quit-buffer ()
"Quit the raw view and return to the message."
(interactive)
(if (buffer-live-p mm/view-buffer)
(switch-to-buffer mm/view-buffer)
(kill-buffer)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; functions for org-contacts
(defun mm/org-contacts-from (name-or-email)
"Get a message field if we are in view mode; NAME-OR-EMAIL should
be either 'name or 'email to get the corresponding field. If the
field is not found, \"\" is returned. Use this with org-contact
with a template like:
(\"c\" \"Contacts\" entry (file \"~/Org/contacts.org\")
\"* %(mm/org-contacts-from 'name)
:PROPERTIES:
:EMAIL: %(mm/org-contacts-from 'email)
:END:\")))
See the `org-contacts' documentation for more details."
(with-current-buffer mm/view-buffer-name ;; hackish...
(unless (eq major-mode 'mm/view-mode)
(error "Not in mm/view mode."))
(unless mm/current-msg
(error "No current message."))
(let ((from (car-safe (plist-get mm/current-msg :from))))
(cond
((not from) "") ;; nothing found
((eq name-or-email 'name)
(or (car-safe from) ""))
((eq name-or-email 'email)
(or (cdr-safe from) ""))
(t (error "Not supported: %S" name-or-email))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactive functions
(defun mm/view-toggle-wrap-lines ()
"Toggle line wrap in the message body."
(interactive)
(if mm/wrap-lines
(progn
(setq mm/wrap-lines nil)
(mm/view-refresh)) ;; back to normal
(save-excursion
(let ((inhibit-read-only t))
(setq mm/wrap-lines t)
(goto-char (point-min))
(when (search-forward "\n\n") ;; search for the message body
(fill-region (point) (point-max)))))))
(defun mm/view-toggle-hide-cited ()
"Toggle hiding of cited lines in the message body."
(interactive)
(if mm/hide-cited
(progn
(setq mm/hide-cited nil)
(mm/view-refresh))
(save-excursion
(let ((inhibit-read-only t))
(goto-char (point-min))
(flush-lines "^[:blank:]*>")
(setq mm/hide-cited t)))))
(defun mm/view-refresh ()
"Redisplay the current message."
(interactive)
(mm/view mm/current-msg mm/hdrs-buffer t))
(defun mm/view-quit-buffer ()
"Quit the message view and return to the headers."
(interactive)
(if (buffer-live-p mm/hdrs-buffer)
(switch-to-buffer mm/hdrs-buffer)
(kill-buffer)))
(defun mm/view-next-header ()
"View the next header."
(interactive)
(when (mm/next-header)
(mm/view-message)))
(defun mm/view-prev-header ()
"View the previous header."
(interactive)
(when (mm/prev-header)
(mm/view-message)))
(defun mm/view-mark-for-move ()
"Mark the current message for moving."
(interactive)
(when (mm/mark-for-move)
(mm/view-message)))
(defun mm/view-mark-for-trash ()
"Mark the current message for moving to the trash folder."
(interactive)
(when (mm/mark-for-trash)
(mm/view-message)))
(defun mm/view-mark-for-delete ()
"Mark the current message for deletion."
(interactive)
(when (mm/mark-for-delete)
(mm/view-message)))
(defun mm/view-extract-attachment (attnum)
"Extract the attachment with ATTNUM."
(unless mm/attachment-dir (error "`mm/attachment-dir' is not set"))
(when (or (null mm/attach-map) (zerop (hash-table-count mm/attach-map)))
(error "No attachments for this message"))
(interactive "nAttachment to extract:")
(let* ((att (gethash attnum mm/attach-map))
(path (and att (concat mm/attachment-dir
"/" (plist-get att :name))))
(id (and att (plist-get att :index)))
(retry t))
(unless att (error "Not a valid attachment number"))
(while retry
(setq path (expand-file-name (read-string "Save as " path)))
(setq retry
(and (file-exists-p path)
(not (y-or-n-p (concat "Overwrite " path "?"))))))
(mm/proc-save (plist-get mm/current-msg :docid) id path)))
(defun mm/view-open-attachment (attnum)
"Extract the attachment with ATTNUM"
(unless mm/attach-map
(error "No attachments for this message"))
(interactive "nAttachment to open:")
(let* ((att (gethash attnum mm/attach-map))
(id (and att (plist-get att :index))))
(unless id (error "Not a valid attachment number"))
(mm/proc-open (plist-get mm/current-msg :docid) id)))
(defun mm/view-unmark ()
"Warn user that unmarking only works in the header list."
(interactive)
(message "Unmarking needs to be done in the header list view"))
(defun mm/view-marked-execute ()
"Warn user that execution can only take place in n the header
list."
(interactive)
(message "Execution needs to be done in the header list view"))
(defun mm/view-go-to-url (num)
"Go to a numbered url."
(interactive "nGo to url with number: ")
(let ((url (gethash num mm/link-map)))
(unless url (error "Invalid number for URL"))
(browse-url url)))
(defun mm/view-raw ()
"Show the the raw text of the current message."
(interactive)
(unless mm/current-msg
(error "No current message"))
(mm/view-raw-message mm/current-msg (current-buffer)))
(defun mm/view-pipe (cmd)
"Pipe the message through shell command CMD, and display the
results."
(interactive "sShell command: ")
(unless mm/current-msg
(error "No current message"))
(mm/view-shell-command-on-raw-message mm/current-msg (current-buffer) cmd))
(defconst mm/muile-buffer-name "*muile*"
"Name of the buffer to execute muile.")
(defconst mm/muile-process-name "*muile*"
"Name of the muile process.")
;; note, implementation is very basic/primitive; we probably need comint to do
;; something like geiser does (http://www.nongnu.org/geiser/). Desirable
;; features: a) the output is not editable b) tab-completions work
(defun mm/inspect-message ()
"Inspect the current message in the Guile/Muile shell."
(interactive)
(unless mm/muile-binary (error "`mm/muile-binary' is not defined"))
(unless (or (file-executable-p mm/muile-binary)
(executable-find mm/muile-binary))
(error "%S not found" mm/muile-binary))
(unless mm/current-msg
(error "No current message"))
(get-buffer-create mm/muile-buffer-name)
(start-process mm/muile-buffer-name mm/muile-process-name
mm/muile-binary "--msg" (plist-get mm/current-msg :path))
(switch-to-buffer mm/muile-buffer-name)
(shell-mode))
(provide 'mm-view)

479
emacs/mm.el Normal file
View File

@ -0,0 +1,479 @@
;;; mm.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:
;;; Code:
(eval-when-compile (require 'cl))
(require 'mm-hdrs)
(require 'mm-view)
(require 'mm-main)
(require 'mm-send)
(require 'mm-proc)
(require 'mm-version) ;; auto-generated
;; mm-version.el is autogenerated, and defines mm/mu-version
(require 'mm-version)
;; Customization
(defgroup mm nil
"mm - the mu mail client"
:group 'local)
(defcustom mm/mu-home nil
"Location of the mu homedir, or nil for the default."
:type 'directory
:group 'mm
:safe 'stringp)
(defcustom mm/mu-binary "mu"
"Name of the mu-binary to use; if it cannot be found in your
PATH, you can specify the full path."
:type 'file
:group 'mm
:safe 'stringp)
(defcustom mm/maildir nil
"Your Maildir directory. When `nil', mu will try to find it."
:type 'directory
:safe 'stringp
:group 'mm)
(defcustom mm/get-mail-command nil
"Shell command to run to retrieve new mail; e.g. 'offlineimap' or
'fetchmail'."
:type 'string
:group 'mm
:safe 'stringp)
(defcustom mm/attachment-dir (expand-file-name "~/")
"Default directory for saving attachments."
:type 'string
:group 'mm
:safe 'stringp)
(defvar mm/user-mail-address-regexp "$^"
"Regular expression matching the user's mail address(es). This is
used to distinguish ourselves from others, e.g. when replying and
in :from-or-to headers. By default, match nothing.")
(defvar mm/date-format-long "%c"
"Date format to use in the message view, in the format of
`format-time-string'.")
(defvar mm/search-results-limit 500
"Maximum number of search results (or -1 for unlimited). Since
limiting search results speeds up searches significantly, it's
useful to limit this. Note, to ignore the limit, use a prefix
argument (C-u) before invoking the search.")
(defvar mm/debug nil
"When set to non-nil, log debug information to the *mm-log* buffer.")
(defvar mm/bookmarks
'( ("flag:unread AND NOT flag:trashed" "Unread messages" ?u)
("date:today..now" "Today's messages" ?t)
("date:7d..now" "Last 7 days" ?w)
("mime:image/*" "Messages with images" ?p))
"A list of pre-defined queries; these will show up in the main
screen. Each of the list elements is a three-element list of the
form (QUERY DESCRIPTION KEY), where QUERY is a string with a mu
query, DESCRIPTION is a short description of the query (this will
show up in the UI), and KEY is a shortcut key for the query.")
;; Folders
(defgroup mm/folders nil
"Special folders for mm."
:group 'mm)
;; (defcustom mm/inbox-folder nil
;; "Your Inbox folder, relative to `mm/maildir', e.g. \"/Inbox\"."
;; :type 'string
;; :safe 'stringp
;; :group 'mm/folders)
(defcustom mm/sent-folder nil
"Your folder for sent messages, relative to `mm/maildir',
e.g. \"/Sent Items\"."
:type 'string
:safe 'stringp
:group 'mm/folders)
(defcustom mm/draft-folder nil
"Your folder for draft messages, relative to `mm/maildir',
e.g. \"/drafts\""
:type 'string
:safe 'stringp
:group 'mm/folders)
(defcustom mm/trash-folder nil
"Your folder for trashed messages, relative to `mm/maildir',
e.g. \"/trash\"."
:type 'string
:safe 'stringp
:group 'mm/folders)
(defcustom mm/maildir-shortcuts nil
"A list of maildir shortcuts to enable quickly going to the
particular for, or quickly moving messages towards them (i.e.,
archiving or refiling). The list contains elements of the form
(maildir . shortcut), where MAILDIR is a maildir (such as
\"/archive/\"), and shortcut a single shortcut character. With
this, in the header buffer and view buffer you can execute
`mm/mark-for-move-quick' (or 'm', by default) or
`mm/jump-to-maildir-quick' (or 'j', by default), followed by the
designated shortcut character for the maildir.")
;; the headers view
(defgroup mm/headers nil
"Settings for the headers view."
:group 'mm)
(defcustom mm/headers-fields
'( (:date . 25)
(:flags . 6)
(:from . 22)
(:subject . 40))
"A list of header fields to show in the headers buffer, and their
respective widths in characters. A width of `nil' means
'unrestricted', and this is best reserved fo the rightmost (last)
field. For the complete list of available headers, see `mm/header-names'"
:type (list 'symbol)
:group 'mm/headers)
(defcustom mm/headers-date-format "%x %X"
"Date format to use in the headers view, in the format of
`format-time-string'."
:type 'string
:group 'mm/headers)
;; the message view
(defgroup mm/view nil
"Settings for the message view."
:group 'mm)
(defcustom mm/view-fields
'(:from :to :cc :subject :flags :date :maildir :path :attachments)
"Header fields to display in the message view buffer. For the
complete list of available headers, see `mm/header-names'."
:type (list 'symbol)
:group 'mm/view)
(defcustom mm/view-date-format "%c"
"Date format to use in the message view, in the format of
`format-time-string'."
:type 'string
:group 'mm/headers)
;; Composing / Sending messages
(defgroup mm/compose nil
"Customizations for composing/sending messages."
:group 'mm)
(defcustom mm/msg-citation-prefix "> "
"String to prefix cited message parts with."
:type 'string
:group 'mm/compose)
(defcustom mm/msg-reply-prefix "Re: "
"String to prefix the subject of replied messages with."
:type 'string
:group 'mm/compose)
(defcustom mm/msg-forward-prefix "Fwd: "
"String to prefix the subject of forwarded messages with."
:type 'string
:group 'mm/compose)
(defcustom mm/user-agent nil
"The user-agent string; leave at `nil' for the default."
:type 'string
:group 'mm/compose)
;; Faces
(defgroup mm/faces nil
"Faces used in by mm."
:group 'mm
:group 'faces)
(defface mm/unread-face
'((t :inherit font-lock-keyword-face :bold t))
"Face for an unread mm message header."
:group 'mm/faces)
(defface mm/moved-face
'((t :inherit font-lock-comment-face :slant italic))
"Face for an mm message header that has been moved to some
folder (it's still visible in the search results, since we cannot
be sure it no longer matches)."
:group 'mm/faces)
(defface mm/trashed-face
'((t :inherit font-lock-comment-face :strike-through t))
"Face for an message header in the trash folder."
:group 'mm/faces)
(defface mm/draft-face
'((t :inherit font-lock-string-face))
"Face for a draft message header (i.e., a message with the draft
flag set)."
:group 'mm/faces)
(defface mm/header-face
'((t :inherit default))
"Face for an mm header without any special flags."
:group 'mm/faces)
(defface mm/title-face
'((t :inherit font-lock-type-face))
"Face for an mm title."
:group 'mm/faces)
(defface mm/view-header-key-face
'((t :inherit font-lock-builtin-face))
"Face for the header title (such as \"Subject\" in the message view)."
:group 'mm/faces)
(defface mm/view-header-value-face
'((t :inherit font-lock-doc-face))
"Face for the header value (such as \"Re: Hello!\" in the message view)."
:group 'mm/faces)
(defface mm/view-link-face
'((t :inherit font-lock-type-face :underline t))
"Face for showing URLs and attachments in the message view."
:group 'mm/faces)
(defface mm/highlight-face
'((t :inherit font-lock-pseudo-keyword-face :bold t))
"Face for highlighting things."
:group 'mm/faces)
(defface mm/view-url-number-face
'((t :inherit font-lock-reference-face :bold t))
"Face for the number tags for URLs."
:group 'mm/faces)
(defface mm/view-attach-number-face
'((t :inherit font-lock-variable-name-face :bold t))
"Face for the number tags for attachments."
:group 'mm/faces)
(defface mm/view-footer-face
'((t :inherit font-lock-comment-face))
"Face for message footers (signatures)."
:group 'mm/faces)
(defface mm/hdrs-marks-face
'((t :inherit font-lock-preprocessor-face))
"Face for the mark in the headers list."
:group 'mm/faces)
(defface mm/system-face
'((t :inherit font-lock-comment-face :slant italic))
"Face for system message (such as the footers for message
headers)."
:group 'mm/faces)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; internal variables / constants
(defconst mm/header-names
'( (:attachments . "Attach")
(:bcc . "Bcc")
(:cc . "Cc")
(:date . "Date")
(:flags . "Flags")
(:from . "From")
(:from-or-to . "From/To")
(:maildir . "Maildir")
(:path . "Path")
(:subject . "Subject")
(:to . "To"))
"A alist of all possible header fields; this is used in the UI (the
column headers in the header list, and the fields the message
view). Most fields should be self-explanatory. A special one is
`:from-or-to', which is equal to `:from' unless `:from' matches ,
in which case it will be equal to `:to'.)")
;; mm startup function ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm ()
"Start mm. We do this by sending a 'ping' to the mu server
process, and start the main view if the 'pong' we receive from the
server has the expected values."
(interactive)
(if (buffer-live-p mm/main-buffer-name)
(switch-to-buffer mm/main-buffer-name)
(setq mm/proc-pong-func
(lambda (version doccount)
(unless (string= version mm/mu-version)
(error "mu server has version %s, but we need %s"
version mm/mu-version))
(mm/main-view)))
(mm/proc-ping)))
(defun mm/ask-maildir (prompt)
"Ask the user for a shortcut (using PROMPT) as defined in
`mm/maildir-shortcuts', then return the corresponding folder
name. If the special shortcut 'o' (for _o_ther) is used, or if
`mm/maildir-shortcuts is not defined, let user choose from all
maildirs under `mm/maildir."
(unless mm/maildir (error "`mm/maildir' is not defined"))
(if (not mm/maildir-shortcuts)
(ido-completing-read prompt (mm/get-sub-maildirs mm/maildir))
(let* ((mlist (append mm/maildir-shortcuts '(("ther" . ?o))))
(fnames
(mapconcat
(lambda (item)
(concat
"["
(propertize (make-string 1 (cdr item)) 'face 'mm/view-link-face)
"]"
(car item)))
mlist ", "))
(kar (read-char (concat prompt fnames))))
(if (= kar ?o) ;; user chose 'other'?
(ido-completing-read prompt (mm/get-sub-maildirs mm/maildir))
(or
(car-safe (find-if (lambda (item) (= kar (cdr item))) mm/maildir-shortcuts))
(error "Invalid shortcut '%c'" kar))))))
(defun mm/ask-bookmark (prompt)
"Ask the user for a bookmark (using PROMPT) as defined in
`mm/bookmarks', then return the corresponding query."
(unless mm/bookmarks (error "`mm/bookmarks' is not defined"))
(let* ((bmarks
(mapconcat
(lambda (bm)
(let ((query (nth 0 bm)) (title (nth 1 bm)) (key (nth 2 bm)))
(concat
"[" (propertize (make-string 1 key) 'face 'mm/view-link-face) "]"
title))) mm/bookmarks ", "))
(kar (read-char (concat prompt bmarks)))
(chosen-bm
(find-if (lambda (bm) (= kar (nth 2 bm))) mm/bookmarks)))
(unless chosen-bm (error "Invalid shortcut '%c'" kar))
(nth 0 chosen-bm)))
(defun mm/new-buffer (bufname)
"Return a new buffer BUFNAME; if such already exists, kill the
old one first."
(when (get-buffer bufname)
(progn
(message (format "Killing %s" bufname))
(kill-buffer bufname)))
(get-buffer-create bufname))
;;; converting flags->string and vice-versa ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mm/flags-to-string (flags)
"Remove duplicates and sort the output of `mm/flags-to-string-raw'."
(concat
(sort (remove-duplicates (append (mm/flags-to-string-raw flags) nil)) '>)))
(defun mm/flags-to-string-raw (flags)
"Convert a list of flags into a string as seen in Maildir
message files; flags are symbols draft, flagged, new, passed,
replied, seen, trashed and the string is the concatenation of the
uppercased first letters of these flags, as per [1]. Other flags
than the ones listed here are ignored.
Also see `mm/flags-to-string'.
\[1\]: http://cr.yp.to/proto/maildir.html"
(when flags
(let ((kar (case (car flags)
('draft ?D)
('flagged ?F)
('new ?N)
('passed ?P)
('replied ?R)
('seen ?S)
('trashed ?T)
('attach ?a)
('encrypted ?x)
('signed ?s)
('unread ?u))))
(concat (and kar (string kar))
(mm/flags-to-string-raw (cdr flags))))))
(defun mm/string-to-flags (str)
"Remove duplicates from the output of `mm/string-to-flags-1'"
(remove-duplicates (mm/string-to-flags-1 str)))
(defun mm/string-to-flags-1 (str)
"Convert a string with message flags as seen in Maildir
messages into a list of flags in; flags are symbols draft,
flagged, new, passed, replied, seen, trashed and the string is
the concatenation of the uppercased first letters of these flags,
as per [1]. Other letters than the ones listed here are ignored.
Also see `mu/flags-to-string'.
\[1\]: http://cr.yp.to/proto/maildir.html"
(when (/= 0 (length str))
(let ((flag
(case (string-to-char str)
(?D 'draft)
(?F 'flagged)
(?P 'passed)
(?R 'replied)
(?S 'seen)
(?T 'trashed))))
(append (when flag (list flag))
(mm/string-to-flags-1 (substring str 1))))))
(defun mm/display-size (size)
"Get a string representation of SIZE (in bytes)."
(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))
(t "<unknown>")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'mm)

38
emacs/mm.org Normal file
View File

@ -0,0 +1,38 @@
* mm
** Introduction
Welcome to *mu mail* - an emacs client for the [[http://www.djcbsoftware.nl/code/mu][mu]] maildir indexing/searching
tool. It turns mu into an e-mail-client.
Mu Mail has things in common with programs such as 'notmuch' and 'md', but -
in the opinion of it's author - it offers some unique features as
well. Basically, the mail handling (deleting, moving etc.) is inspired by
*Wanderlust* (another emacs-based e-mail client) and *dired*, while it takes
some cues from GMail with respect to being search-based. In practice this
means that mu mail provides a 'traditional' folder-based e-mail client, on
top of a search based back-end.
** How does it work
While not necessarily interesting for all users of mu mail, for some it may
be interesting to know how mu mail does its job.
Since version 0.9.8, mu has a special =server= command, which drops you into
a command line where you can give certain commands to mu (see the =mu-server=
man page). While it would certainly be possible to have specific commands to
get lists of messages, move them, delete them etc., having a running instance
around gets rid of the startup time of mu and especially the message
database.
So, when running mu mail inside emacs, it fires up an instance of 'mu
server', and communicates with it as long as it runs.
mu mail shows its results only after the mu server reports their
completion. Still, the execution is asynchronous, so you do not need wait for
anything. We found that, for example, deleting messages is fast enough to
allow us to wait for the results. An alternative design would be to update
the user-interface already; so far we are quite content with the performance.
Regarding performance, showing large numbers (thousands) of message can still
be a bit slower than is desirable. This is an area for improvement still.