;; notmuch-address.el --- address completion with notmuch
;;
;; Copyright © David Edmondson
;;
;; This file is part of Notmuch.
;;
;; Notmuch 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.
;;
;; Notmuch 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 Notmuch. If not, see .
;;
;; Authors: David Edmondson
(require 'message)
(require 'notmuch-query)
(require 'notmuch-parser)
;;
(defcustom notmuch-address-command nil
"The command which generates possible addresses for completion.
It must take a single argument and output a list of possible
matches, one per line. If set to nil, addresses are generated by
a built-in completion mechanism."
:type '(radio (const :tag "No command: Use built-in completion" nil)
(string :tag "Custom command" :value "notmuch-addresses"))
:group 'notmuch-send
:group 'notmuch-external)
(defcustom notmuch-address-selection-function 'notmuch-address-selection-function
"The function to select address from given list. The function is
called with PROMPT, COLLECTION, and INITIAL-INPUT as arguments
(subset of what `completing-read' can be called with).
While executed the value of `completion-ignore-case' is t.
See documentation of function `notmuch-address-selection-function'
to know how address selection is made by default."
:type 'function
:group 'notmuch-send
:group 'notmuch-external)
(defvar notmuch-address-completions nil
"Hash of email addresses for completion during email composition.
This variable is set by calling `notmuch-address-harvest'.")
(defun notmuch-address-selection-function (prompt collection initial-input)
"Call (`completing-read'
PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)"
(completing-read
prompt collection nil nil initial-input 'notmuch-address-history))
(defvar notmuch-address-message-alist-member
'("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):"
. notmuch-address-expand-name))
(defvar notmuch-address-history nil)
(defun notmuch-address-message-insinuate ()
(unless (memq notmuch-address-message-alist-member message-completion-alist)
(setq message-completion-alist
(push notmuch-address-message-alist-member message-completion-alist))))
(defun notmuch-address-options (original)
(cond
((eq notmuch-address-command nil)
(let ((candidates))
(maphash (lambda (key val)
(let ((re (concat "\\<" (regexp-quote original))))
(when (string-match re key)
(push key candidates))))
notmuch-address-completions)
candidates))
(t (process-lines notmuch-address-command original))))
(defun notmuch-address-expand-name ()
(let* ((end (point))
(beg (save-excursion
(re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
(goto-char (match-end 0))
(point)))
(orig (buffer-substring-no-properties beg end))
(completion-ignore-case t)
(options (notmuch-address-options orig))
(num-options (length options))
(chosen (cond
((eq num-options 0)
nil)
((eq num-options 1)
(car options))
(t
(funcall notmuch-address-selection-function
(format "Address (%s matches): " num-options)
(cdr options) (car options))))))
(if chosen
(progn
(push chosen notmuch-address-history)
(delete-region beg end)
(insert chosen))
(message "No matches.")
(ding))))
;; Copied from `w3m-which-command'.
(defun notmuch-address-locate-command (command)
"Return non-nil if `command' is an executable either on
`exec-path' or an absolute pathname."
(when (stringp command)
(if (and (file-name-absolute-p command)
(file-executable-p command))
command
(setq command (file-name-nondirectory command))
(catch 'found-command
(let (bin)
(dolist (dir exec-path)
(setq bin (expand-file-name command dir))
(when (or (and (file-executable-p bin)
(not (file-directory-p bin)))
(and (file-executable-p (setq bin (concat bin ".exe")))
(not (file-directory-p bin))))
(throw 'found-command bin))))))))
(defun notmuch-address-harvest-msg (msg)
(let* ((headers (plist-get msg :headers))
(to (ignore-errors (mail-extract-address-components (plist-get headers :To) t)))
(cc (ignore-errors (mail-extract-address-components (plist-get headers :Cc) t)))
(bcc (ignore-errors (mail-extract-address-components (plist-get headers :Bcc) t))))
(mapc (lambda (parts)
(let* ((name (car parts))
(email (cadr parts))
(entry (if name (format "%s <%s>" name email) email)))
(puthash entry t notmuch-address-completions)))
(append to cc bcc))
nil))
(defun notmuch-address-harvest-handle-result (obj)
(notmuch-query-map-threads 'notmuch-address-harvest-msg (list obj)))
(defun notmuch-address-harvest-filter (proc string)
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(save-excursion
(goto-char (point-max))
(insert string))
(notmuch-sexp-parse-partial-list
'notmuch-address-harvest-handle-result (process-buffer proc)))))
(defvar notmuch-address-harvest-proc nil) ; the process of a harvest underway
(defun notmuch-address-harvest ()
"Collect possible addresses for completion. It queries the
notmuch database for all emails sent by the user and collects the
destination addresses from them in
`notmuch-address-completions'. This takes some time so the
address collection runs asynchronously."
(when notmuch-address-harvest-proc
(kill-buffer (process-buffer notmuch-address-harvest-proc)) ; this also kills the process
(setq notmuch-address-harvest-proc nil))
(setq notmuch-address-completions (make-hash-table :test 'equal))
(setq notmuch-address-harvest-proc
(notmuch-start-notmuch
"notmuch-address-harvest" ; process name
" *notmuch-address-harvest*" ; process buffer
nil ; process sentinel
"show" ; notmuch command
"--format=sexp"
"--format-version=2"
"--body=false"
"--entire-thread=false"
(mapconcat (lambda (x) (concat "from:" x)) (notmuch-user-emails) " or ")))
(set-process-filter notmuch-address-harvest-proc 'notmuch-address-harvest-filter)
(set-process-query-on-exit-flag notmuch-address-harvest-proc nil)
;; return value
nil)
;; If we can find the program specified by `notmuch-address-command'
;; or if it is nil, insinuate ourselves into `message-mode'.
(when (or (eq notmuch-address-command nil)
(notmuch-address-locate-command notmuch-address-command))
(notmuch-address-message-insinuate))
(defun notmuch-address-harvest-start ()
(when (not notmuch-address-completions) (notmuch-address-harvest)))
(when (eq notmuch-address-command nil)
(add-hook 'message-mode-hook 'notmuch-address-harvest-start))
;;
(provide 'notmuch-address)