mu4e: support incremental contacts

This commit is contained in:
djcb
2019-05-11 13:35:22 +03:00
parent 9edcae0203
commit 7563b89c9c
4 changed files with 225 additions and 267 deletions

View File

@ -262,6 +262,45 @@ message. Return nil if there are no recipients for the particular field."
(otherwise (otherwise
(mu4e-error "Unsupported field"))))) (mu4e-error "Unsupported field")))))
;;; RFC2822 handling of phrases in mail-addresses
;;; The optional display-name contains a phrase, it sits before the angle-addr
;;; as specified in RFC2822 for email-addresses in header fields.
;;; contributed by jhelberg
(defun mu4e~rfc822-phrase-type (ph)
"Return either atom, quoted-string, a corner-case or nil. This
checks for empty string first. Then quotes around the phrase
(returning 'rfc822-quoted-string). Then whether there is a quote
inside the phrase (returning 'rfc822-containing-quote).
The reverse of the RFC atext definition is then tested.
If it matches, nil is returned, if not, it is an 'rfc822-atom, which
is returned."
(cond
((= (length ph) 0) 'rfc822-empty)
((= (aref ph 0) ?\")
(if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
'rfc822-quoted-string
'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
((string-match-p "[\"]" ph) 'rfc822-containing-quote)
((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
(t 'rfc822-atom)))
(defun mu4e~rfc822-quoteit (ph)
"Quote RFC822 phrase only if necessary.
Atoms and quoted strings don't need quotes. The rest do. In
case a phrase contains a quote, it will be escaped."
(let ((type (mu4e~rfc822-phrase-type ph)))
(cond
((eq type 'rfc822-atom) ph)
((eq type 'rfc822-quoted-string) ph)
((eq type 'rfc822-containing-quote)
(format "\"%s\""
(replace-regexp-in-string "\"" "\\\\\"" ph)))
(t (format "\"%s\"" ph)))))
(defun mu4e~draft-from-construct () (defun mu4e~draft-from-construct ()
"Construct a value for the From:-field of the reply to MSG, "Construct a value for the From:-field of the reply to MSG,

View File

@ -187,7 +187,8 @@ The server output is as follows:
;; note: we use 'member', to match (:contacts nil) ;; note: we use 'member', to match (:contacts nil)
((plist-member sexp :contacts) ((plist-member sexp :contacts)
(funcall mu4e-contacts-func (funcall mu4e-contacts-func
(plist-get sexp :contacts))) (plist-get sexp :contacts)
(plist-get sexp :tstamp)))
;; something got moved/flags changed ;; something got moved/flags changed
((plist-get sexp :update) ((plist-get sexp :update)
@ -495,15 +496,17 @@ to a temporary file, then respond with
"Sends a ping to the mu server, expecting a (:pong ...) in response." "Sends a ping to the mu server, expecting a (:pong ...) in response."
(mu4e~proc-send-command "cmd:ping")) (mu4e~proc-send-command "cmd:ping"))
(defun mu4e~proc-contacts (personal after) (defun mu4e~proc-contacts (personal after tstamp)
"Sends the contacts command to the mu server. "Ask for contacts with PERSONAL AFTER TSTAMP.
A (:contacts (<list>)) is expected in response. If PERSONAL is S-expression (:contacts (<list>) :tstamp \"<tstamp>\") is expected in
non-nil, only get personal contacts, if AFTER is non-nil, get response. If PERSONAL is non-nil, only get personal contacts, if
only contacts seen AFTER (the time_t value)." AFTER is non-nil, get only contacts seen AFTER (the time_t
value)."
(mu4e~proc-send-command (mu4e~proc-send-command
"cmd:contacts personal:%s after:%d" "cmd:contacts personal:%s after:%d tstamp:%s"
(if personal "true" "false") (if personal "true" "false")
(or after 0))) (or after 0)
(or tstamp "0")))
(defun mu4e~proc-view (docid-or-msgid &optional images decrypt) (defun mu4e~proc-view (docid-or-msgid &optional images decrypt)
"Get a message DOCID-OR-MSGID. "Get a message DOCID-OR-MSGID.

View File

@ -1,7 +1,6 @@
;;; mu4e-utils.el -- part of mu4e, the mu mail user agent ;;; mu4e-utils.el -- part of mu4e, the mu mail user agent
;; ;;
;; Copyright (C) 2011-2017 Dirk-Jan C. Binnema ;; Copyright (C) 2011-2019 Dirk-Jan C. Binnema
;; Copyright (C) 2013 Tibor Simko
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
@ -666,7 +665,8 @@ process."
"Indexing completed; processed %d, updated %d, cleaned-up %d" "Indexing completed; processed %d, updated %d, cleaned-up %d"
(plist-get info :processed) (plist-get info :updated) (plist-get info :processed) (plist-get info :updated)
(plist-get info :cleaned-up)) (plist-get info :cleaned-up))
(unless (zerop (plist-get info :updated)) (unless (and (not (string= mu4e~contacts-tstamp "0"))
(zerop (plist-get info :updated)))
(mu4e~request-contacts-maybe) (mu4e~request-contacts-maybe)
(run-hooks 'mu4e-index-updated-hook))))) (run-hooks 'mu4e-index-updated-hook)))))
((plist-get info :message) ((plist-get info :message)
@ -680,121 +680,39 @@ process."
(t (error "Error %d: %s" errcode errmsg)))) (t (error "Error %d: %s" errcode errmsg))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; RFC2822 handling of phrases in mail-addresses (defvar mu4e~contacts-tstamp "0"
;;; The optional display-name contains a phrase, it sits before the angle-addr "Timestamp for the most recent contacts update." )
;;; as specified in RFC2822 for email-addresses in header fields.
;;; contributed by jhelberg
(defun mu4e~rfc822-phrase-type (ph) (defun mu4e~update-contacts (contacts &optional tstamp)
"Return either atom, quoted-string, a corner-case or nil. This "Rceive a sorted list of CONTACTS.
checks for empty string first. Then quotes around the phrase Each of the contacts has the form
(returning 'rfc822-quoted-string). Then whether there is a quote (FULL_EMAIL_ADDRESS . RANK) and fill the hash
inside the phrase (returning 'rfc822-containing-quote). `mu4e~contacts' with it, with each contact mapped to an integer
The reverse of the RFC atext definition is then tested. for their ranking.
If it matches, nil is returned, if not, it is an 'rfc822-atom, which
is returned."
(cond
((= (length ph) 0) 'rfc822-empty)
((= (aref ph 0) ?\")
(if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
'rfc822-quoted-string
'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
((string-match-p "[\"]" ph) 'rfc822-containing-quote)
((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
(t 'rfc822-atom)))
(defun mu4e~rfc822-quoteit (ph)
"Quote RFC822 phrase only if necessary.
Atoms and quoted strings don't need quotes. The rest do. In
case a phrase contains a quote, it will be escaped."
(let ((type (mu4e~rfc822-phrase-type ph)))
(cond
((eq type 'rfc822-atom) ph)
((eq type 'rfc822-quoted-string) ph)
((eq type 'rfc822-containing-quote)
(format "\"%s\""
(replace-regexp-in-string "\"" "\\\\\"" ph)))
(t (format "\"%s\"" ph)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defsubst mu4e~process-contact (contact)
"Process CONTACT, and either return nil when it should not be included,
or (rfc822-string . CONTACT) otherwise."
(when mu4e-contact-rewrite-function
(setq contact (funcall mu4e-contact-rewrite-function contact)))
(when contact
(let ((name (plist-get contact :name))
(mail (plist-get contact :mail))
(ignore-rx (or mu4e-compose-complete-ignore-address-regexp "$^")))
(when (and mail (not (string-match ignore-rx mail)))
(cons
(if name (format "%s <%s>" (mu4e~rfc822-quoteit name) mail) mail)
contact)))))
(defun mu4e~sort-contacts (contacts)
"Destructively sort contacts (only for cycling) in order of
'mostly likely contact'.t See the code for the detail"
(let* ((now (+ (float-time) 3600)) ;; allow for clock diffs
(recent (- (float-time) (* 15 24 3600))))
(cl-sort contacts
(lambda (c1 c2)
(let* ( (c1 (cdr c1)) (c2 (cdr c2))
(personal1 (plist-get c1 :personal))
(personal2 (plist-get c2 :personal))
;; note: freq, tstamp can only be missing if the rewrite
;; function removed them. If the rewrite function changed the
;; contact somehow, we guess it's important.
(freq1 (or (plist-get c1 :freq) 500))
(freq2 (or (plist-get c2 :freq) 500))
(tstamp1 (or (plist-get c1 :tstamp) now))
(tstamp2 (or (plist-get c2 :tstamp) now)))
;; only one is personal? if so, that one comes first
(if (not (equal personal1 personal2))
(if personal1 t nil)
;; only one is recent? that one comes first
(if (not (equal (> tstamp1 recent) (> tstamp2 recent)))
(> tstamp1 tstamp2)
;; otherwise, use the frequency
(> freq1 freq2))))))))
(defun mu4e~sort-contacts-for-completion (contacts)
"Takes CONTACTS, which is a list of RFC-822 addresses, and sort them based
on the ranking in `mu4e~contacts.'"
(cl-sort contacts
(lambda (c1 c2)
(let ((rank1 (gethash c1 mu4e~contacts))
(rank2 (gethash c2 mu4e~contacts)))
(< rank1 rank2)))))
;; start and stopping
(defun mu4e~fill-contacts (contact-data)
"We receive a list of contacts, which each contact of the form
(:me NAME :mail EMAIL :tstamp TIMESTAMP :freq FREQUENCY) and
fill the hash `mu4e~contacts' with it, with each contact mapped
to an integer for their ranking.
This is used by the completion function in mu4e-compose." This is used by the completion function in mu4e-compose."
(let ((contacts) (rank 0)) ;; We have our nicely sorted list, map them to a list
(dolist (contact contact-data)
(let ((contact-maybe (mu4e~process-contact contact)))
;; note, this gives cells (rfc822-address . contact)
(when contact-maybe (push contact-maybe contacts))))
(setq contacts (mu4e~sort-contacts contacts))
;; now, we have our nicely sorted list, map them to a list
;; of increasing integers. We use that map in the composer ;; of increasing integers. We use that map in the composer
;; to sort them there. It would have been so much easier if emacs ;; to sort them there. It would have been so much easier if emacs
;; allowed us to use the sorted-list as-is, but no such luck. ;; allowed us to use the sorted-list as-is, but no such luck.
(let ((n 0))
(unless mu4e~contacts
(setq mu4e~contacts (make-hash-table :test 'equal :weakness nil (setq mu4e~contacts (make-hash-table :test 'equal :weakness nil
:size (length contacts))) :size (length contacts))))
(dolist (contact contacts) (dolist (contact contacts)
(puthash (car contact) rank mu4e~contacts) (incf n)
(incf rank)) (let ((address
(mu4e-index-message "Contacts received: %d" (if (functionp mu4e-contact-process-function)
(hash-table-count mu4e~contacts)))) (funcall mu4e-contact-process-function (car contact))
(car contact))))
(when address
(puthash address (cdr contact) mu4e~contacts))))
(setq mu4e~contacts-tstamp (or tstamp "0"))
(mu4e-index-message "Contacts updated: %d; total %d"
n (hash-table-count mu4e~contacts))))
(defun mu4e~check-requirements () (defun mu4e~check-requirements ()
"Check for the settings required for running mu4e." "Check for the settings required for running mu4e."
@ -855,21 +773,21 @@ Checks whether the server process is live."
the list of contacts we use for autocompletion; otherwise, do the list of contacts we use for autocompletion; otherwise, do
nothing." nothing."
(when mu4e-compose-complete-addresses (when mu4e-compose-complete-addresses
(setq mu4e-contacts-func 'mu4e~fill-contacts) (setq mu4e-contacts-func 'mu4e~update-contacts)
(mu4e~proc-contacts (mu4e~proc-contacts
mu4e-compose-complete-only-personal mu4e-compose-complete-only-personal
(when mu4e-compose-complete-only-after (when mu4e-compose-complete-only-after
(float-time (float-time
(apply 'encode-time (apply 'encode-time
(mu4e-parse-time-string mu4e-compose-complete-only-after))))))) (mu4e-parse-time-string mu4e-compose-complete-only-after))))
mu4e~contacts-tstamp)))
(defun mu4e~start (&optional func) (defun mu4e~start (&optional func)
"If `mu4e-contexts' have been defined, but we don't have a "If `mu4e-contexts' have been defined, but we don't have a
context yet, switch to the matching one, or none matches, the context yet, switch to the matching one, or none matches, the
first. first. If mu4e is already running, execute function FUNC (if
If mu4e is already running, execute function FUNC (if non-nil). non-nil). Otherwise, check various requireme`'nts, then start mu4e.
Otherwise, check various requirements, then start mu4e. When When successful, call FUNC (if non-nil) afterwards."
successful, call FUNC (if non-nil) afterwards."
;; if we're already running, simply go to the main view ;; if we're already running, simply go to the main view
(if (mu4e-running-p) ;; already running? (if (mu4e-running-p) ;; already running?
(when func (funcall func)) ;; yes! run func if defined (when func (funcall func)) ;; yes! run func if defined
@ -905,7 +823,8 @@ successful, call FUNC (if non-nil) afterwards."
"Clear any cached resources." "Clear any cached resources."
(setq (setq
mu4e-maildir-list nil mu4e-maildir-list nil
mu4e~contacts nil)) mu4e~contacts nil
mu4e~contacts-tstamp "0"))
(defun mu4e~stop () (defun mu4e~stop ()
"Stop the mu4e session." "Stop the mu4e session."

View File

@ -1,6 +1,6 @@
;;; mu4e-vars.el -- part of mu4e, the mu mail user agent ;;; mu4e-vars.el -- part of mu4e, the mu mail user agent
;; ;;
;; Copyright (C) 2011-2018 Dirk-Jan C. Binnema ;; Copyright (C) 2011-2019 Dirk-Jan C. Binnema
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
@ -353,7 +353,7 @@ addresses)."
:type 'boolean :type 'boolean
:group 'mu4e-compose) :group 'mu4e-compose)
(defcustom mu4e-compose-complete-only-after "2010-01-01" (defcustom mu4e-compose-complete-only-after "2014-01-01"
"Consider only contacts last seen after this date. "Consider only contacts last seen after this date.
Date must be a string, in a format parseable by Date must be a string, in a format parseable by
`org-parse-time-string'. This excludes really old contacts. `org-parse-time-string'. This excludes really old contacts.
@ -373,34 +373,31 @@ their canonical counterpart; useful as an example."
(mail (plist-get contact :mail))) (mail (plist-get contact :mail)))
(list :name name :mail mail))) (list :name name :mail mail)))
(defcustom mu4e-contact-rewrite-function nil (make-obsolete-variable 'mu4e-contacts-rewrite-function
"Function for rewriting or removing contacts. "mu4e-contact-process-function (see docstring)" "mu4e 1.3.2")
(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp
"mu4e-contact-process-function (see docstring)" "mu4e 1.3.2")
If the function receives the contact as a list of the form (defcustom mu4e-contact-process-function nil
(:name NAME :mail EMAIL ... other properties ... ) "Function for processing contact information for use in auto-completion.
(other properties may be there as well)
The function receives the contact as a string, e.g
\"Foo Bar <foo.bar@example.com>\"
\"cuux@example.com\"
The function should return either: The function should return either:
- nil: remove this contact, or - nil: do not use this contact for completion
- the rewritten cell, or - the (possibly rewritten) address, which must be
- the existing cell as-is an RFC-2822-compatible e-mail address."
For rewriting, it is recommended to use `plist-put' to set the
changed parameters, so the other properties stay in place. Those
are needed for sorting the contacts."
:type 'function :type 'function
:group 'mu4e-compose) :group 'mu4e-compose)
(defcustom mu4e-compose-complete-ignore-address-regexp "no-?reply" (defcustom mu4e-compose-reply-ignore-address
"Ignore any e-mail addresses for completion if they match this regexp." message-dont-reply-to-names
:type 'string
:group 'mu4e-compose)
(defcustom mu4e-compose-reply-ignore-address message-dont-reply-to-names
"Addresses to prune when doing wide replies. "Addresses to prune when doing wide replies.
This can be a regexp matching the address, a list of regexps This can be a regexp matching the address, a list of regexps or a
or a predicate function. A value of nil keeps all the addresses." predicate function. A value of nil keeps all the addresses."
:type '(choice :type '(choice
(const nil) (const nil)
function function