EUDC: Add ecomplete and mailabbrev backends

* doc/misc/eudc.texi (Overview): Add ecomplete and mailabbrev
nodes.
(ecomplete, mailabbrev): New nodes.
(Installation): Add ecomplete and mailabbrev nodes.
(LDAP Configuration): Use code formatting instead of quotes.
(macOS Contacts Configuration): Likewise.
(ecomplete Configuration): New node.
(mailabbrev Configuration): Likewise.
* etc/NEWS (EUDC): Mention ecomplete and mailabbrev backends,
mention eudc-server-hotlist default change.
* lisp/net/eudc-vars.el (eudc-known-protocols): Add ecomplete and
mailabbrev.
(eudc-server-hotlist): Add entries for ecomplete and mailabbrev.
* lisp/net/eudcb-ecomplete.el: New EUDC backend file.
* lisp/net/eudcb-mailabbrev.el: Likewise.
* test/lisp/net/eudc-resources/ecompleterc,
test/lisp/net/eudc-resources/mailrc: New eudc-tests resource
files.
* test/lisp/net/eudc-tests.el (eudc-test-rfc5322-quote-phrase)
(eudc-test-make-address, eudcb-ecomplete, eudcb-mailabbrev): New
test cases.
This commit is contained in:
Alexander Adolf 2022-11-08 13:39:19 -05:00 committed by Thomas Fitzsimmons
parent 1f53a5f1b3
commit 0e25a39e69
8 changed files with 485 additions and 11 deletions

View file

@ -85,6 +85,10 @@ LDAP, Lightweight Directory Access Protocol
BBDB, Big Brother's Insidious Database
@item
macOS Contacts
@item
@code{ecomplete}, Emacs's electrical completion
@item
@code{mailabbrev}, Emacs's abbrev-expansion of mail aliases
@end itemize
The main features of the EUDC interface are:
@ -110,6 +114,8 @@ Interface to BBDB to let you insert server records into your own BBDB database
* LDAP:: What is LDAP ?
* BBDB:: What is BBDB ?
* macOS Contacts:: What is macOS Contacts ?
* ecomplete:: What is @code{ecomplete} ?
* mailabbrev:: What is @code{mailabbrev}?
@end menu
@ -173,14 +179,73 @@ Address Book; the EUDC macOS Contacts back end also works on those
older versions.
@node ecomplete
@section @code{ecomplete}
@code{ecomplete} is Emacs's ``electric completion'', and it is part of
Emacs. It stores all information in an @file{ecompleterc} file, whose
location, and name can be configured via the variable
@code{ecomplete-database-file} (which see). The format of the file
is:
@display
((TYPE_1 ITEM_1 ITEM_2 ...)
(TYPE_2 ITEM_N+1 ITEM_N+2 ...)
...)
@end display
That is, it is an alist map where the key is the type of match (so
that you can have one list of things for ``mail'', and one for, say,
``mastodon''). In each of these sections you then have a list where
each item is of the form:
@display
(KEY TIMES-USED LAST-TIME-USED STRING)
@end display
When performing a query, the result will be all items where the search
term matches all, or part of STRING.
When EUDC performs queries with @code{ecomplete}, the name of each
attribute making up the query is used as the type in which the lookup
is performed. The mapping from EUDC attribute names to
@code{ecomplete} type names is performed according to the variable
@code{eudc-ecomplete-attributes-translation-alist} (which see).
@node mailabbrev
@section @code{mailabbrev}
@code{mailabbrev} is Emacs's ``abbrev-expansion of mail aliases'', and
it is part of Emacs. It stores all information in a @file{mailrc}
file, whose location, and name can be configured via the variable
@code{mail-personal-alias-file} (which see). The @file{mailrc} file
has the same format as the @command{mail} and @command{mailx} commands
use for their startup configuration file. @code{mailabbrev} processes
@samp{alias}, and @samp{source} statements in the @file{mailrc} file.
@samp{alias} statements can define simple aliases and distribution
lists, and and can be nested in that the alias expansion can contain
references to other alias definitions. Forward references, that is
references to aliases before they are actually defined, are possible,
too.
Originally, @code{mailabbrev} was designed to be used with
@code{abbrev-mode}. The @code{mailabbrev} EUDC backend does not use
@code{abbrev-mode}, but queries @code{mailabbrev} for alias entries
only, and returns these as EUDC results. All entries where the alias
name exactly equals either the @code{email}, @code{name}, or
@code{firstname} attribute value in the EUDC query, will be returned
as matches. When a @file{mailrc} alias defines a distribution list,
that is it expands to more than one email address, the EUDC result
will contain a single entry, which will contain an email attribute
only, whose value will be a comma-separated list of RFC 5322 formatted
recipient specifications.
@node Installation
@chapter Installation
Add the following to your @file{.emacs} init file:
@lisp
(require 'eudc)
@end lisp
This will install EUDC at startup.
EUDC is built-in to Emacs, and its main functions are autoloaded.
After installing EUDC you will find (the next time you launch Emacs) a
new @code{Directory Search} submenu in the @samp{Tools} menu that will
@ -200,6 +265,8 @@ email composition buffers (@pxref{Inline Query Expansion})
@menu
* LDAP Configuration:: EUDC needs external support for LDAP
* macOS Contacts Configuration:: Enable the macOS Contacts backend
* ecomplete Configuration:: Enable the ecomplete backend
* mailabbrev Configuration:: Enable the mailabbrev backend
@end menu
@node LDAP Configuration
@ -256,7 +323,7 @@ will return all LDAP entries with surnames that begin with
@code{Smith}. In every LDAP query it makes, EUDC implicitly appends
the wildcard character to the end of the last word, except if the word
corresponds to an attribute which is a member of
`eudc-ldap-no-wildcard-attributes'.
@code{eudc-ldap-no-wildcard-attributes}.
@menu
* Emacs-only Configuration:: Configure with @file{.emacs}
@ -406,9 +473,9 @@ level to 5 by appending @code{-d 5} to the command line.
macOS Contacts support is added by means of @file{eudcb-mab.el}, or
@file{eudcb-macos-contacts.el} which are part of Emacs.
To enable a macOS Contacts backend, first `require' the respective
library to load it, and then set the `eudc-server' to localhost in
your init file:
To enable a macOS Contacts backend, first @code{require} the
respective library to load it, and then set the @code{eudc-server} to
localhost in your init file:
@lisp
(require 'eudcb-macos-contacts)
(eudc-macos-contacts-set-server "localhost")
@ -433,6 +500,32 @@ command-line utility before upgrading to a new version of macOS.
existing configurations, and may be removed in a future release.
@node ecomplete Configuration
@section @code{ecomplete} Configuration
@code{ecomplete} is Emacs's ``electrical completion'', and is part of
Emacs. To use it, you will need to set up a database file
(@pxref{ecomplete}) first.
It will be autoloaded on demand.
You can also enable multi-server queries as described in
@pxref{Multi-server Queries}.
@node mailabbrev Configuration
@section @code{mailabbrev} Configuration
@code{mailabbrev} is Emacs's ``abbrev-expansion of mail aliases'', and
it is part of Emacs. To use it, you will need to set up a database file
(@pxref{mailabbrev}) first.
It will be autoloaded on demand.
You can also enable multi-server queries as described in
@pxref{Multi-server Queries}.
@node Usage
@chapter Usage

View file

@ -2017,6 +2017,25 @@ The EUDC back-end for the macOS Contacts app now provides a wider set
of attributes to use for queries, and delivers more attributes in
query results.
+++
*** New back-end for ecomplete
A new back-end for ecomplete allows information from that database to
be queried by EUDC, too. The attributes present in the EUDC query are
used to select the entry type in the ecomplete database.
+++
*** New back-end for mailabbrev
A new back-end for mailabbrev allows information from that database to
be queried by EUDC, too. The attributes email, name, and firstname
are supported only.
+++
*** New default for 'eudc-server-hotlist' includes built-in backends
The 'eudc-server-hotlist' user option now defaults to including
entries for the new built-in ecomplete and mailabbrev EUDC backends.
As a result, 'C-u M-x eudc-expand-try-all' will query both of these
backends for email address completions, by default.
** EWW/SHR
+++

View file

@ -51,9 +51,10 @@ instead."
;; Known protocols (used in completion)
;; Not to be mistaken with `eudc-supported-protocols'
(defvar eudc-known-protocols '(bbdb ldap))
(defvar eudc-known-protocols '(bbdb ldap ecomplete mailabbrev))
(defcustom eudc-server-hotlist nil
(defcustom eudc-server-hotlist '(("localhost" . ecomplete)
("localhost" . mailabbrev))
"Directory servers to query.
This is an alist of the form (SERVER . PROTOCOL). SERVER is the
host name or URI of the server, PROTOCOL is a symbol representing

108
lisp/net/eudcb-ecomplete.el Normal file
View file

@ -0,0 +1,108 @@
;;; eudcb-ecomplete.el --- EUDC - ecomplete backend -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;;
;; Author: Alexander Adolf
;;
;; This file is 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library provides an interface to the ecomplete package as
;; an EUDC data source.
;;; Usage:
;; No setup is required, since there is an entry for this backend
;; in `eudc-server-hotlist' by default.
;;
;; For example, if your `ecomplete-database-file' (typically
;; ~/.emacs.d/ecompleterc) contains:
;;
;; ((mail ("larsi@gnus.org" 38154 1516109510 "Lars <larsi@ecomplete.org>")))
;;
;; Then:
;;
;; C-x m lars C-u M-x eudc-expand-try-all RET
;;
;; should expand the email address into the To: field of the new
;; message.
;;; Code:
(require 'eudc)
(require 'ecomplete)
(require 'mail-parse)
(defvar eudc-ecomplete-attributes-translation-alist
'((email . mail))
"See `eudc-protocol-attributes-translation-alist'.
The back-end-specific attribute names are used as the \"type\" of
entry when searching, and they must hence match the types you use
in your ecompleterc database file.")
;; hook ourselves into the EUDC framework
(eudc-protocol-set 'eudc-query-function
'eudc-ecomplete-query-internal
'ecomplete)
(eudc-protocol-set 'eudc-list-attributes-function
nil
'ecomplete)
(eudc-protocol-set 'eudc-protocol-attributes-translation-alist
'eudc-ecomplete-attributes-translation-alist
'ecomplete)
(eudc-protocol-set 'eudc-protocol-has-default-query-attributes
nil
'ecomplete)
;;;###autoload
(defun eudc-ecomplete-query-internal (query &optional _return-attrs)
"Query `ecomplete' with QUERY.
QUERY is a list of cons cells (ATTR . VALUE). Since `ecomplete'
does not provide attributes in the usual sense, the
back-end-specific attribute names in
`eudc-ecomplete-attributes-translation-alist' are used as the
KEY (that is, the \"type\" of match) when looking for matches in
`ecomplete-database'.
RETURN-ATTRS is ignored." ; FIXME: why is this being ignored?
(ecomplete-setup)
(let ((email-attr (car (eudc-translate-attribute-list '(email))))
result)
(dolist (term query)
(let* ((attr (car term))
(value (cdr term))
(matches (ecomplete-get-matches attr value)))
(when matches
(dolist (match (split-string (string-trim (substring-no-properties
matches))
"[\n\r]"))
;; Try to decompose the email address.
(let* ((decoded (mail-header-parse-address match t))
(name (cdr decoded))
(email (car decoded)))
(if (and decoded (eq attr email-attr))
;; The email could be decomposed, push individual
;; fields.
(push `((,attr . ,email)
,@(when name (list (cons 'name name))))
result)
;; Otherwise just forward the value as-is.
(push (list (cons attr match)) result)))))))
result))
(eudc-register-protocol 'ecomplete)
(provide 'eudcb-ecomplete)
;;; eudcb-ecomplete.el ends here

View file

@ -0,0 +1,127 @@
;;; eudcb-mailabbrev.el --- EUDC - mailabbrev backend -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;;
;; Author: Alexander Adolf
;;
;; This file is 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library provides an interface to the mailabbrev package as
;; an EUDC data source.
;;; Usage:
;; No setup is required, since there is an entry for this backend
;; in `eudc-server-hotlist' by default.
;;
;; For example, if your `mail-personal-alias-file' (typically
;; ~/.mailrc) contains:
;;
;; alias lars "Lars <larsi@mail-abbrev.com>"
;;
;; Then:
;;
;; C-x m lars C-u M-x eudc-expand-try-all RET
;;
;; will expand the correct email address into the To: field of the
;; new message.
;;; Code:
(require 'eudc)
(require 'mailabbrev)
(require 'mail-parse)
;; hook ourselves into the EUDC framework
(eudc-protocol-set 'eudc-query-function
'eudc-mailabbrev-query-internal
'mailabbrev)
(eudc-protocol-set 'eudc-list-attributes-function
nil
'mailabbrev)
(eudc-protocol-set 'eudc-protocol-attributes-translation-alist
nil
'mailabbrev)
(eudc-protocol-set 'eudc-protocol-has-default-query-attributes
nil
'mailabbrev)
;;;###autoload
(defun eudc-mailabbrev-query-internal (query &optional _return-attrs)
"Query `mailabbrev' with QUERY.
QUERY is a list of cons cells (ATTR . VALUE). Since `mailabbrev'
does not provide attributes in the usual sense, only the email,
name, and firstname attributes in the QUERY are considered, and
their values are matched against the alias names in the mailrc
file. When a mailrc alias is a distribution list, that is it
expands to more that one email address, the individual recipient
specifications are formatted using `eudc-rfc5322-make-address',
and returned as a comma-separated list in the email address
attribute.
RETURN-ATTRS is a list of attributes to return, defaulting to
`eudc-default-return-attributes'."
(mail-abbrevs-setup)
(let (result)
(dolist (term query)
(let* ((attr (car term))
(value (cdr term))
(raw-matches (symbol-value (intern-soft value mail-abbrevs))))
(when (and raw-matches
(memq attr '(email firstname name)))
(let* ((matches (split-string raw-matches ", "))
(num-matches (length matches)))
(if (> num-matches 1)
;; multiple matches: distribution list
(let ((distr-str (string)))
(dolist (recipient matches)
;; try to decompose email construct
(let* ((decoded (mail-header-parse-address recipient t))
(name (cdr decoded))
(email (car decoded)))
(if decoded
;; decoding worked, push rfc5322 rendered address
(setq distr-str
(copy-sequence
(concat distr-str ", "
(eudc-rfc5322-make-address email
nil
name))))
;; else, just forward the value as-is
(setq distr-str
(copy-sequence
(concat distr-str ", " recipient))))))
;; push result, removing the leading ", "
(push (list (cons 'email (substring distr-str 2 -1)))
result))
;; simple case: single match
(let* ((match (car matches))
(decoded (mail-header-parse-address match t))
(name (cdr decoded))
(email (car decoded)))
(if decoded
;; decoding worked, push individual fields
(push `((email . ,email)
,@(when name (list (cons 'name name))))
result)
;; else, just forward the value as-is
(push (list (cons 'email match)) result))))))))
result))
(eudc-register-protocol 'mailabbrev)
(provide 'eudcb-mailabbrev)
;;; eudcb-mailabbrev.el ends here

View file

@ -0,0 +1,7 @@
((mail
("larsi@gnus.org" 38154 1516109510 "Lars Ingebrigtsen <larsi@ecomplete.org>")
("kfogel@red-bean.com" 10 1516065455 "Karl Fogel <kfogel@ecomplete.com>")
("behse@ecomplete.org" 10 1516065455 "behse@ecomplete.org"))
(phone
("Lars Ingebrigtsen" 0 0 "+1 234 5678 9012")
("Karl Fogel" 0 0 "+33 701 4567 8901")))

View file

@ -0,0 +1,3 @@
alias lars "Lars Ingebrigtsen <larsi@mail-abbrev.com>"
alias karl "Karl Fogel <kfogel@mail-abbrev.com>"
alias emacsheroes lars karl

View file

@ -152,4 +152,120 @@
(should (eq 'b (eudc-lax-plist-get '(nil a "a" a) 'a 'b)))
(should (eq 'b (eudc-lax-plist-get '(a nil "nil" nil) nil 'b)))))
;; eudc-rfc5322-quote-phrase (string)
(ert-deftest eudc-test-rfc5322-quote-phrase ()
"Tests for RFC5322 compliant phrase quoting."
;; atext-token "[:alpha:][:digit:]!#$%&'*+/=?^_`{|}~-"
(should (equal (eudc-rfc5322-quote-phrase "Foo Bar !#$%&'*+/=?^_`{|}~-")
"Foo Bar !#$%&'*+/=?^_`{|}~-"))
(should (equal (eudc-rfc5322-quote-phrase "Foo, Bar !#$%&'*+/=?^_`{|}~-")
"\"Foo, Bar !#$%&'*+/=?^_`{|}~-\"")))
;; eudc-rfc5322-valid-comment-p (string)
(ert-deftest eudc-test-rfc5322-valid-comment-p ()
"Tests for RFC5322 compliant comments."
;; cctext-token "\u005D-\u007E\u002A-\u005B\u0021-\u0027" + fwsp-token (TAB, LF, SPC)
;; Printable US-ASCII characters not including "(", ")", or "\".
(let ((good-chars (append (number-sequence #x09 #x0a)
(number-sequence #x20 #x20)
(number-sequence #x21 #x27)
(number-sequence #x2a #x5b)
(number-sequence #x5d #x7e)))
(bad-chars (append (number-sequence #x00 #x08)
(number-sequence #x0b #x1f)
(number-sequence #x28 #x29)
(number-sequence #x5c #x5c)
(number-sequence #x7f #xff))))
(dolist (gc good-chars)
(should (eq (eudc-rfc5322-valid-comment-p (format "%c" gc)) t)))
(dolist (bc bad-chars)
(should (eq (eudc-rfc5322-valid-comment-p (format "%c" bc)) nil)))))
;; eudc-rfc5322-make-address (address &optional firstname name comment)
(ert-deftest eudc-test-make-address ()
"Tests for RFC5322 compliant email address formatting."
(should (equal (eudc-rfc5322-make-address "")
nil))
(should (equal (eudc-rfc5322-make-address nil)
nil))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org")
"j.sixpack@example.org"))
(should (equal (eudc-rfc5322-make-address "<j.sixpack@example.org>")
"<j.sixpack@example.org>"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
"Joey")
"Joey <j.sixpack@example.org>"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
"Joey"
"Sixpack")
"Joey Sixpack <j.sixpack@example.org>"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
"Joey"
"Sixpack"
"ten-packs are fine, too")
"Joey Sixpack <j.sixpack@example.org> \
(ten-packs are fine, too)"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
""
"Sixpack, Joey")
"\"Sixpack, Joey\" <j.sixpack@example.org>"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
nil
"Sixpack, Joey")
"\"Sixpack, Joey\" <j.sixpack@example.org>"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
nil
nil
"Duh!")
"j.sixpack@example.org (Duh!)"))
(should (equal (eudc-rfc5322-make-address "j.sixpack@example.org"
nil
nil
"Duh\\!")
"j.sixpack@example.org")))
(require 'ert-x) ; ert-with-temp-directory
(defvar ecomplete-database-file (ert-resource-file "ecompleterc"))
(ert-deftest eudcb-ecomplete ()
"Test the ecomplete back-end."
(ert-with-temp-directory home
(with-environment-variables (("HOME" home))
(let ((eudc-ignore-options-file t))
(should (equal (eudc-ecomplete-query-internal '((mail . "brigts")))
'(((mail . "Lars Ingebrigtsen <larsi@ecomplete.org>")))))
(should (equal (eudc-ecomplete-query-internal '((mail . "karl")))
'(((mail . "Karl Fogel <kfogel@ecomplete.com>")))))
(should (equal (eudc-ecomplete-query-internal '((mail . "behs")))
'(((mail . "behse@ecomplete.org")))))
(should (equal (eudc-ecomplete-query-internal '((mail . "louie")))
nil))))))
(ert-with-temp-directory
home
(ert-deftest eudcb-mailabbrev ()
"Test the mailabbrev back-end."
(with-environment-variables
(("HOME" home))
(let ((mail-personal-alias-file (ert-resource-file "mailrc"))
(eudc-ignore-options-file t))
(should (equal (eudc-mailabbrev-query-internal '((email . "lars")))
'(((email . "larsi@mail-abbrev.com")
(name . "Lars Ingebrigtsen")))))
(should (equal (eudc-mailabbrev-query-internal '((name . "lars")))
'(((email . "larsi@mail-abbrev.com")
(name . "Lars Ingebrigtsen")))))
(should (equal (eudc-mailabbrev-query-internal '((phone . "lars")))
nil))
(should (equal (eudc-mailabbrev-query-internal '((firstname . "karl")))
'(((email . "kfogel@mail-abbrev.com")
(name . "Karl Fogel")))))
(should (equal (eudc-mailabbrev-query-internal '((email . "louie")))
nil))
(should (equal (eudc-mailabbrev-query-internal '((name . "emacsheroes")))
'(((email . "Lars Ingebrigtsen <larsi@mail-abbrev.com>, \
Karl Fogel <kfogel@mail-abbrev.com")))))))))
(provide 'eudc-tests)
;;; eudc-tests.el ends here