2023-09-20 21:31:19 +02:00
|
|
|
|
;;; mu4e-contacts.el -- Dealing with contacts -*- lexical-binding: t -*-
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
2023-09-20 21:31:19 +02:00
|
|
|
|
;; Copyright (C) 2022-2023 Dirk-Jan C. Binnema
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
|
|
|
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
|
|
|
|
|
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
|
|
|
|
|
|
;; mu4e 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.
|
|
|
|
|
|
|
|
|
|
;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
|
|
|
|
;; Utility functions used in the mu4e
|
|
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
(require 'cl-lib)
|
2023-10-19 06:43:15 +02:00
|
|
|
|
(require 'message)
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(require 'mu4e-helpers)
|
2021-08-30 17:28:52 +02:00
|
|
|
|
(require 'mu4e-update)
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Configuration
|
|
|
|
|
(defcustom mu4e-compose-complete-addresses t
|
|
|
|
|
"Whether to do auto-completion of e-mail addresses."
|
|
|
|
|
:type 'boolean
|
|
|
|
|
:group 'mu4e-compose)
|
|
|
|
|
|
|
|
|
|
(defcustom mu4e-compose-complete-only-personal nil
|
2022-05-27 20:00:37 +02:00
|
|
|
|
"Whether to consider only \"personal\" e-mail addresses for completion.
|
2021-08-29 16:30:10 +02:00
|
|
|
|
That is, addresses from messages where user was explicitly in one
|
|
|
|
|
of the address fields (this excludes mailing list messages).
|
2022-05-27 20:00:37 +02:00
|
|
|
|
These addresses are the ones specified with \"mu init\"."
|
2021-08-29 16:30:10 +02:00
|
|
|
|
:type 'boolean
|
|
|
|
|
:group 'mu4e-compose)
|
|
|
|
|
|
2022-05-05 00:32:46 +02:00
|
|
|
|
(defcustom mu4e-compose-complete-only-after "2018-01-01"
|
2021-08-29 16:30:10 +02:00
|
|
|
|
"Consider only contacts last seen after this date.
|
|
|
|
|
|
2022-05-05 00:32:46 +02:00
|
|
|
|
Date must be a string of the form YYYY-MM-DD.
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
This is useful for limiting a potentially enormous set of
|
|
|
|
|
contacts for auto-completion to just those that are present in
|
2022-05-05 00:32:46 +02:00
|
|
|
|
the e-mail corpus in recent times. Set to nil to not have any
|
2021-08-29 16:30:10 +02:00
|
|
|
|
time-based restriction."
|
|
|
|
|
:type 'string
|
|
|
|
|
:group 'mu4e-compose)
|
|
|
|
|
|
2022-05-12 07:52:10 +02:00
|
|
|
|
(defcustom mu4e-compose-complete-max nil
|
2023-05-10 06:05:16 +02:00
|
|
|
|
"Limit the amount of contacts for completion, nil for no limits.
|
|
|
|
|
After considering the other constraints
|
|
|
|
|
\(`mu4e-compose-complete-addresses' and
|
2022-05-12 07:52:10 +02:00
|
|
|
|
`mu4e-compose-complete-only-after'), pick only the highest-ranked
|
|
|
|
|
<n>.
|
2022-05-06 20:58:51 +02:00
|
|
|
|
|
2023-05-10 06:05:16 +02:00
|
|
|
|
Lowering this variable reduces start-up time and memory usage."
|
|
|
|
|
:type '(choice natnum (const :tag "No limits" nil))
|
2022-05-06 20:58:51 +02:00
|
|
|
|
:group 'mu4e-compose)
|
|
|
|
|
|
2021-08-29 16:30:10 +02:00
|
|
|
|
;; names and mail-addresses can be mapped onto their canonical
|
|
|
|
|
;; counterpart. use the customizeable function
|
|
|
|
|
;; mu4e-canonical-contact-function to do that. below the identity
|
|
|
|
|
;; function for mapping a contact onto the canonical one.
|
|
|
|
|
(defun mu4e-contact-identity (contact)
|
|
|
|
|
"Return the name and the mail-address of a CONTACT.
|
|
|
|
|
It is used as the identity function for converting contacts to
|
|
|
|
|
their canonical counterpart; useful as an example."
|
|
|
|
|
(let ((name (plist-get contact :name))
|
|
|
|
|
(mail (plist-get contact :mail)))
|
|
|
|
|
(list :name name :mail mail)))
|
|
|
|
|
|
|
|
|
|
(defcustom mu4e-contact-process-function
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(lambda(addr)
|
|
|
|
|
(cond
|
|
|
|
|
((string-match-p "reply" addr)
|
|
|
|
|
;; no-reply adresses are not useful of course, but neither are are
|
|
|
|
|
;; reply-xxxx addresses since they're autogenerated only useful for direct
|
|
|
|
|
;; replies.
|
|
|
|
|
nil)
|
|
|
|
|
(t addr)))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
"Function for processing contact information for use in auto-completion.
|
|
|
|
|
|
2022-05-09 20:22:17 +02:00
|
|
|
|
The function receives the contact as a string, e.g \"Foo Bar
|
|
|
|
|
<foo.bar@example.com>\" \"cuux@example.com\"
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
The function should return either:
|
|
|
|
|
- nil: do not use this contact for completion
|
|
|
|
|
- the (possibly rewritten) address, which must be
|
|
|
|
|
an RFC-2822-compatible e-mail address."
|
|
|
|
|
:type 'function
|
|
|
|
|
:group 'mu4e-compose)
|
|
|
|
|
|
|
|
|
|
(defcustom mu4e-compose-reply-ignore-address
|
|
|
|
|
'("no-?reply")
|
|
|
|
|
"Addresses to prune when doing wide replies.
|
|
|
|
|
|
|
|
|
|
This can be a regexp matching the address, a list of regexps or a
|
|
|
|
|
predicate function. A value of nil keeps all the addresses."
|
|
|
|
|
:type '(choice
|
|
|
|
|
(const nil)
|
|
|
|
|
function
|
|
|
|
|
string
|
|
|
|
|
(repeat string))
|
|
|
|
|
:group 'mu4e-compose)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Internal variables
|
|
|
|
|
(defvar mu4e--contacts-tstamp "0"
|
|
|
|
|
"Timestamp for the most recent contacts update." )
|
|
|
|
|
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(defvar mu4e--contacts-set nil
|
|
|
|
|
"Set with the full contact addresses for autocompletion.")
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
;;; user mail address
|
2023-05-10 05:59:10 +02:00
|
|
|
|
(defun mu4e-personal-addresses (&optional no-regexp)
|
2023-10-28 10:43:21 +02:00
|
|
|
|
"Get the list user's personal addresses, as passed to \"mu init\".
|
|
|
|
|
|
|
|
|
|
The address are either plain e-mail addresses or regexps (strings
|
|
|
|
|
wrapped / /). When NO-REGEXP is non-nil, do not include regexp
|
2021-08-29 16:30:10 +02:00
|
|
|
|
address patterns (if any)."
|
|
|
|
|
(seq-remove
|
2023-05-10 05:59:10 +02:00
|
|
|
|
(lambda (addr) (and no-regexp (string-match-p "^/.*/" addr)))
|
|
|
|
|
(when-let ((props (mu4e-server-properties)))
|
|
|
|
|
(plist-get props :personal-addresses))))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
(defun mu4e-personal-address-p (addr)
|
|
|
|
|
"Is ADDR a personal address?
|
2023-05-10 05:59:10 +02:00
|
|
|
|
Evaluate to nil if ADDR does not match any of the personal
|
|
|
|
|
addresses. Uses \\=(mu4e-personal-addresses) for the addresses
|
|
|
|
|
with both the plain addresses and /regular expressions/."
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(when addr
|
|
|
|
|
(seq-find
|
|
|
|
|
(lambda (m)
|
|
|
|
|
(if (string-match "/\\(.*\\)/" m)
|
|
|
|
|
(let ((rx (match-string 1 m))
|
|
|
|
|
(case-fold-search t))
|
2023-05-10 05:59:10 +02:00
|
|
|
|
(string-match rx addr))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(eq t (compare-strings addr nil nil m nil nil 'case-insensitive))))
|
|
|
|
|
(mu4e-personal-addresses))))
|
|
|
|
|
|
2023-10-29 16:04:21 +01:00
|
|
|
|
(defun mu4e-personal-or-alternative-address-p (addr)
|
|
|
|
|
"Is ADDR either a personal or an alternative address?
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
2023-10-29 16:04:21 +01:00
|
|
|
|
That is, does it match either `mu4e-personal-address-p' or
|
|
|
|
|
`message-alternative-emails'.
|
2023-10-28 10:43:21 +02:00
|
|
|
|
|
2023-10-29 16:04:21 +01:00
|
|
|
|
Note that this expanded definition of user-addresses not used for
|
|
|
|
|
indexing mu does not know about `message-alternative-emails' so
|
|
|
|
|
it cannot use it for indexing."
|
|
|
|
|
(let ((alts message-alternative-emails))
|
|
|
|
|
(or (mu4e-personal-address-p addr)
|
|
|
|
|
(cond
|
|
|
|
|
((functionp alts) (funcall alts addr))
|
|
|
|
|
((stringp alts) (string-match alts addr))
|
|
|
|
|
(t nil)))))
|
2022-05-05 00:32:46 +02:00
|
|
|
|
|
|
|
|
|
;; Helpers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; 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 an atom or quoted-string for the phrase PH.
|
|
|
|
|
This checks for empty string first. Then quotes around the phrase
|
2022-05-27 20:00:37 +02:00
|
|
|
|
\(returning symbol `rfc822-quoted-string'). Then whether there is
|
|
|
|
|
a quote inside the phrase (returning symbol
|
|
|
|
|
`rfc822-containing-quote').
|
|
|
|
|
|
|
|
|
|
The reverse of the RFC atext definition is then tested. If it
|
|
|
|
|
matches, nil is returned, if not, it returns a symbol
|
|
|
|
|
`rfc822-atom'."
|
2022-05-05 00:32:46 +02:00
|
|
|
|
(cond
|
|
|
|
|
((= (length ph) 0) 'rfc822-empty)
|
|
|
|
|
((= (aref ph 0) ?\")
|
|
|
|
|
(if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
|
|
|
|
|
'rfc822-quoted-string
|
2023-10-19 06:43:15 +02:00
|
|
|
|
'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
|
2022-05-05 00:32:46 +02:00
|
|
|
|
((string-match-p "[\"]" ph) 'rfc822-containing-quote)
|
|
|
|
|
((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
|
|
|
|
|
(t 'rfc822-atom)))
|
|
|
|
|
|
|
|
|
|
(defun mu4e--rfc822-quote-phrase (ph)
|
|
|
|
|
"Quote an RFC822 phrase PH 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-contact-name (contact)
|
|
|
|
|
"Get the name of this CONTACT, or nil."
|
|
|
|
|
(plist-get contact :name))
|
|
|
|
|
|
|
|
|
|
(defsubst mu4e-contact-email (contact)
|
|
|
|
|
"Get the name of this CONTACT, or nil."
|
|
|
|
|
(plist-get contact :email))
|
|
|
|
|
|
|
|
|
|
(defsubst mu4e-contact-cons (contact)
|
|
|
|
|
"Convert a CONTACT plist into a old-style (name . email)."
|
|
|
|
|
(cons
|
|
|
|
|
(mu4e-contact-name contact)
|
|
|
|
|
(mu4e-contact-email contact)))
|
|
|
|
|
|
|
|
|
|
(defsubst mu4e-contact-make (name email)
|
|
|
|
|
"Creata contact plist from NAME and EMAIL."
|
|
|
|
|
`(:name ,name :email ,email))
|
|
|
|
|
|
|
|
|
|
(defun mu4e-contact-full (contact)
|
|
|
|
|
"Get the full combination of name and email address from CONTACT."
|
|
|
|
|
(let* ((email (mu4e-contact-email contact))
|
2023-09-20 21:31:19 +02:00
|
|
|
|
(name (mu4e-contact-name contact)))
|
2022-05-05 00:32:46 +02:00
|
|
|
|
(if (and name (> (length name) 0))
|
2023-09-20 21:31:19 +02:00
|
|
|
|
(format "%s <%s>" (mu4e--rfc822-quote-phrase name) email)
|
2022-05-05 00:32:46 +02:00
|
|
|
|
email)))
|
|
|
|
|
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
(defun mu4e--update-contacts (contacts &optional tstamp)
|
|
|
|
|
"Receive a sorted list of CONTACTS newer than TSTAMP.
|
2022-05-09 20:22:17 +02:00
|
|
|
|
Update an internal set with it.
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
This is used by the completion function in mu4e-compose."
|
|
|
|
|
(let ((n 0))
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(unless mu4e--contacts-set
|
|
|
|
|
(setq mu4e--contacts-set (make-hash-table :test 'equal :weakness nil
|
2021-08-29 16:30:10 +02:00
|
|
|
|
:size (length contacts))))
|
|
|
|
|
(dolist (contact contacts)
|
|
|
|
|
(cl-incf n)
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(when (functionp mu4e-contact-process-function)
|
2023-09-20 21:31:19 +02:00
|
|
|
|
(setq contact (funcall mu4e-contact-process-function contact)))
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(when contact ;; note the explicit deccode; the strings we get are
|
2023-09-20 21:31:19 +02:00
|
|
|
|
;; utf-8, but emacs doesn't know yet.
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(puthash (decode-coding-string contact 'utf-8) t mu4e--contacts-set)))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(setq mu4e--contacts-tstamp (or tstamp "0"))
|
|
|
|
|
(unless (zerop n)
|
|
|
|
|
(mu4e-index-message "Contacts updated: %d; total %d"
|
2022-05-09 20:22:17 +02:00
|
|
|
|
n (hash-table-count mu4e--contacts-set)))))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
(defun mu4e-contacts-info ()
|
|
|
|
|
"Display information about the contacts-cache.
|
|
|
|
|
For testing/debugging."
|
|
|
|
|
(interactive)
|
|
|
|
|
(with-current-buffer (get-buffer-create "*mu4e-contacts-info*")
|
|
|
|
|
(erase-buffer)
|
|
|
|
|
(insert (format "complete addresses: %s\n"
|
2023-09-20 21:31:19 +02:00
|
|
|
|
(if mu4e-compose-complete-addresses "yes" "no")))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(insert (format "only personal addresses: %s\n"
|
|
|
|
|
(if mu4e-compose-complete-only-personal "yes" "no")))
|
|
|
|
|
(insert (format "only addresses seen after: %s\n"
|
|
|
|
|
(or mu4e-compose-complete-only-after "no restrictions")))
|
|
|
|
|
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(when mu4e--contacts-set
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(insert (format "number of contacts cached: %d\n\n"
|
2022-05-09 20:22:17 +02:00
|
|
|
|
(hash-table-count mu4e--contacts-set)))
|
|
|
|
|
(maphash (lambda (contact _)
|
2023-09-20 21:31:19 +02:00
|
|
|
|
(insert (format "%s\n" contact))) mu4e--contacts-set))
|
2021-08-29 16:30:10 +02:00
|
|
|
|
(pop-to-buffer "*mu4e-contacts-info*")))
|
|
|
|
|
|
2021-08-29 18:53:53 +02:00
|
|
|
|
(declare-function mu4e--server-contacts "mu4e-server")
|
2021-08-29 16:30:10 +02:00
|
|
|
|
|
|
|
|
|
(defun mu4e--request-contacts-maybe ()
|
2022-05-09 20:22:17 +02:00
|
|
|
|
"Maybe update the set of contacts for autocompletion. |