mu4e: support incremental contacts
This commit is contained in:
@ -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,
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user