From 0e25a39e69acca0324c326ea8e46b1725594bff5 Mon Sep 17 00:00:00 2001 From: Alexander Adolf Date: Tue, 8 Nov 2022 13:39:19 -0500 Subject: [PATCH] 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. --- doc/misc/eudc.texi | 111 ++++++++++++++++++-- etc/NEWS | 19 ++++ lisp/net/eudc-vars.el | 5 +- lisp/net/eudcb-ecomplete.el | 108 +++++++++++++++++++ lisp/net/eudcb-mailabbrev.el | 127 +++++++++++++++++++++++ test/lisp/net/eudc-resources/ecompleterc | 7 ++ test/lisp/net/eudc-resources/mailrc | 3 + test/lisp/net/eudc-tests.el | 116 +++++++++++++++++++++ 8 files changed, 485 insertions(+), 11 deletions(-) create mode 100644 lisp/net/eudcb-ecomplete.el create mode 100644 lisp/net/eudcb-mailabbrev.el create mode 100644 test/lisp/net/eudc-resources/ecompleterc create mode 100644 test/lisp/net/eudc-resources/mailrc diff --git a/doc/misc/eudc.texi b/doc/misc/eudc.texi index 50e483057d3..7293f48f413 100644 --- a/doc/misc/eudc.texi +++ b/doc/misc/eudc.texi @@ -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 diff --git a/etc/NEWS b/etc/NEWS index 60b2caccc84..ab64eff74e3 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -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 +++ diff --git a/lisp/net/eudc-vars.el b/lisp/net/eudc-vars.el index 3ce363cf688..b44989d9061 100644 --- a/lisp/net/eudc-vars.el +++ b/lisp/net/eudc-vars.el @@ -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 diff --git a/lisp/net/eudcb-ecomplete.el b/lisp/net/eudcb-ecomplete.el new file mode 100644 index 00000000000..55011d29f6c --- /dev/null +++ b/lisp/net/eudcb-ecomplete.el @@ -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 . + +;;; 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 "))) +;; +;; 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 diff --git a/lisp/net/eudcb-mailabbrev.el b/lisp/net/eudcb-mailabbrev.el new file mode 100644 index 00000000000..64b50af09bc --- /dev/null +++ b/lisp/net/eudcb-mailabbrev.el @@ -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 . + +;;; 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 " +;; +;; 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 diff --git a/test/lisp/net/eudc-resources/ecompleterc b/test/lisp/net/eudc-resources/ecompleterc new file mode 100644 index 00000000000..9019b26c9f3 --- /dev/null +++ b/test/lisp/net/eudc-resources/ecompleterc @@ -0,0 +1,7 @@ +((mail + ("larsi@gnus.org" 38154 1516109510 "Lars Ingebrigtsen ") + ("kfogel@red-bean.com" 10 1516065455 "Karl Fogel ") + ("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"))) diff --git a/test/lisp/net/eudc-resources/mailrc b/test/lisp/net/eudc-resources/mailrc new file mode 100644 index 00000000000..c565f718372 --- /dev/null +++ b/test/lisp/net/eudc-resources/mailrc @@ -0,0 +1,3 @@ +alias lars "Lars Ingebrigtsen " +alias karl "Karl Fogel " +alias emacsheroes lars karl diff --git a/test/lisp/net/eudc-tests.el b/test/lisp/net/eudc-tests.el index 915006a97c1..c326dcc793f 100644 --- a/test/lisp/net/eudc-tests.el +++ b/test/lisp/net/eudc-tests.el @@ -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 "") + "")) + (should (equal (eudc-rfc5322-make-address "j.sixpack@example.org" + "Joey") + "Joey ")) + (should (equal (eudc-rfc5322-make-address "j.sixpack@example.org" + "Joey" + "Sixpack") + "Joey Sixpack ")) + (should (equal (eudc-rfc5322-make-address "j.sixpack@example.org" + "Joey" + "Sixpack" + "ten-packs are fine, too") + "Joey Sixpack \ +(ten-packs are fine, too)")) + (should (equal (eudc-rfc5322-make-address "j.sixpack@example.org" + "" + "Sixpack, Joey") + "\"Sixpack, Joey\" ")) + (should (equal (eudc-rfc5322-make-address "j.sixpack@example.org" + nil + "Sixpack, Joey") + "\"Sixpack, Joey\" ")) + (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 "))))) + (should (equal (eudc-ecomplete-query-internal '((mail . "karl"))) + '(((mail . "Karl Fogel "))))) + (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 , \ +Karl Fogel