1 ;;; eudc.el --- Emacs Unified Directory Client
3 ;; Copyright (C) 1998 Free Software Foundation, Inc.
5 ;; Author: Oscar Figueiredo <oscar@xemacs.org>
6 ;; Maintainer: Oscar Figueiredo <oscar@xemacs.org>
8 ;; Version: $Revision: 1.23 $
11 ;; This file is part of XEmacs
13 ;; XEmacs is free software; you can redistribute it and/or modify it
14 ;; under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation; either version 2, or (at your option)
18 ;; XEmacs is distributed in the hope that it will be useful, but
19 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 ;; General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with XEmacs; see the file COPYING. If not, write to
25 ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
26 ;; Boston, MA 02111-1307, USA.
29 ;; This package provides a common interface to query directory servers using
30 ;; different protocols such as LDAP, CCSO PH/QI or BBDB. Queries can be
31 ;; made through an interactive form or inline. Inline query strings in
32 ;; buffers are expanded with appropriately formatted query results
33 ;; (especially used to expand email addresses in message buffers). EUDC
34 ;; also interfaces with the BBDB package to let you register query results
35 ;; into your own BBDB database.
38 ;; EUDC comes with an extensive documentation, please refer to it.
40 ;; The main entry points of EUDC are:
41 ;; `eudc-query-form': Query a directory server from a query form
42 ;; `eudc-expand-inline': Query a directory server for the e-mail address
43 ;; of the name before cursor and insert it in the
45 ;; `eudc-get-phone': Get a phone number from a directory server
46 ;; `eudc-get-email': Get an e-mail address from a directory server
47 ;; `eudc-customize': Customize various aspects of EUDC
54 (if (not (fboundp 'make-overlay))
56 (if (not (fboundp 'unless))
59 (unless (fboundp 'custom-menu-create)
60 (autoload 'custom-menu-create "cus-edit"))
66 ;;{{{ Internal cooking
68 ;;{{{ Internal variables and compatibility tricks
70 (defconst eudc-xemacs-p (string-match "XEmacs" emacs-version))
71 (defconst eudc-emacs-p (not eudc-xemacs-p))
72 (defconst eudc-xemacs-mule-p (and eudc-xemacs-p
74 (defconst eudc-emacs-mule-p (and eudc-emacs-p
77 (defvar eudc-form-widget-list nil)
78 (defvar eudc-mode-map nil)
79 ;; Used by the selection insertion mechanism
80 (defvar eudc-pre-select-window-configuration nil)
81 (defvar eudc-insertion-marker nil)
83 ;; List of known servers
84 ;; Alist of (SERVER . PROTOCOL)
85 (defvar eudc-server-hotlist nil)
87 ;; List of variables that have server- or protocol-local bindings
88 (defvar eudc-local-vars nil)
90 ;; Protocol local. Query function
91 (defvar eudc-query-function nil)
93 ;; Protocol local. A function that retrieves a list of valid attribute names
94 (defvar eudc-list-attributes-function nil)
96 ;; Protocol local. A mapping between EUDC attribute names and corresponding
97 ;; protocol specific names. The following names are defined by EUDC and may be
98 ;; included in that list: `name' , `firstname', `email', `phone'
99 (defvar eudc-protocol-attributes-translation-alist nil)
101 ;; Protocol local. Mapping between protocol attribute names and BBDB field
103 (defvar eudc-bbdb-conversion-alist nil)
105 ;; Protocol/Server local. Hook called upon switching to that server
106 (defvar eudc-switch-to-server-hook nil)
108 ;; Protocol/Server local. Hook called upon switching from that server
109 (defvar eudc-switch-from-server-hook nil)
111 ;; Protocol local. Whether the protocol supports queries with no specified
113 (defvar eudc-protocol-has-default-query-attributes nil)
115 (defun eudc-cadr (obj)
118 (defun eudc-cdar (obj)
121 (defun eudc-caar (obj)
124 (defun eudc-cdaar (obj)
125 (cdr (car (car obj))))
127 (defun eudc-plist-member (plist prop)
128 "Return t if PROP has a value specified in PLIST."
129 (if (not (= 0 (% (length plist) 2)))
130 (error "Malformed plist"))
133 (if (eq prop (car plist))
135 (setq plist (cdr (cdr plist))))
138 ;; Emacs' plist-get lacks third parameter
139 (defun eudc-plist-get (plist prop &optional default)
140 "Extract a value from a property list.
141 PLIST is a property list, which is a list of the form
142 (PROP1 VALUE1 PROP2 VALUE2...). This function returns the value
143 corresponding to the given PROP, or DEFAULT if PROP is not
144 one of the properties on the list."
145 (if (eudc-plist-member plist prop)
146 (plist-get plist prop)
149 (defun eudc-lax-plist-get (plist prop &optional default)
150 "Extract a value from a lax property list.
151 PLIST is a lax property list, which is a list of the form (PROP1
152 VALUE1 PROP2 VALUE2...), where comparisons between properties are done
153 using `equal' instead of `eq'. This function returns the value
154 corresponding to PROP, or DEFAULT if PROP is not one of the
155 properties on the list."
156 (if (not (= 0 (% (length plist) 2)))
157 (error "Malformed plist"))
160 (if (equal prop (car plist))
161 (throw 'found (car (cdr plist))))
162 (setq plist (cdr (cdr plist))))
165 (if (not (fboundp 'split-string))
166 (defun split-string (string &optional pattern)
167 "Return a list of substrings of STRING which are separated by PATTERN.
168 If PATTERN is omitted, it defaults to \"[ \\f\\t\\n\\r\\v]+\"."
170 (setq pattern "[ \f\t\n\r\v]+"))
171 (let (parts (start 0))
172 (when (string-match pattern string 0)
173 (if (> (match-beginning 0) 0)
174 (setq parts (cons (substring string 0 (match-beginning 0)) nil)))
175 (setq start (match-end 0))
176 (while (and (string-match pattern string start)
177 (> (match-end 0) start))
178 (setq parts (cons (substring string start (match-beginning 0)) parts)
179 start (match-end 0))))
180 (nreverse (if (< start (length string))
181 (cons (substring string start) parts)
184 (defun eudc-replace-in-string (str regexp newtext)
185 "Replace all matches in STR for REGEXP with NEWTEXT.
186 Value is the new string."
190 (while (setq match (string-match regexp str start))
191 (setq prev-start start
195 (substring str prev-start match)
197 (concat rtn-str (substring str start))))
201 ;;{{{ Server and Protocol Variable Routines
203 (defun eudc-server-local-variable-p (var)
204 "Return non-nil if VAR has server-local bindings."
205 (eudc-plist-member (get var 'eudc-locals) 'server))
207 (defun eudc-protocol-local-variable-p (var)
208 "Return non-nil if VAR has protocol-local bindings."
209 (eudc-plist-member (get var 'eudc-locals) 'protocol))
211 (defun eudc-default-set (var val)
212 "Set the EUDC default value of VAR to VAL.
213 The current binding of VAR is not changed."
214 (put var 'eudc-locals
215 (plist-put (get var 'eudc-locals) 'default val))
216 (add-to-list 'eudc-local-vars var))
218 (defun eudc-protocol-set (var val &optional protocol)
219 "Set the PROTOCOL-local binding of VAR to VAL.
220 If omitted PROTOCOL defaults to the current value of `eudc-protocol'.
221 The current binding of VAR is changed only if PROTOCOL is omitted."
222 (if (eq 'unbound (eudc-variable-default-value var))
223 (eudc-default-set var (symbol-value var)))
224 (let* ((eudc-locals (get var 'eudc-locals))
225 (protocol-locals (eudc-plist-get eudc-locals 'protocol)))
226 (setq protocol-locals (plist-put protocol-locals (or protocol
229 (plist-put eudc-locals 'protocol protocol-locals))
230 (put var 'eudc-locals eudc-locals)
231 (add-to-list 'eudc-local-vars var)
233 (eudc-update-variable var))))
235 (defun eudc-server-set (var val &optional server)
236 "Set the SERVER-local binding of VAR to VAL.
237 If omitted SERVER defaults to the current value of `eudc-server'.
238 The current binding of VAR is changed only if SERVER is omitted."
239 (if (eq 'unbound (eudc-variable-default-value var))
240 (eudc-default-set var (symbol-value var)))
241 (let* ((eudc-locals (get var 'eudc-locals))
242 (server-locals (eudc-plist-get eudc-locals 'server)))
243 (setq server-locals (plist-put server-locals (or server
246 (plist-put eudc-locals 'server server-locals))
247 (put var 'eudc-locals eudc-locals)
248 (add-to-list 'eudc-local-vars var)
250 (eudc-update-variable var))))
253 (defun eudc-set (var val)
254 "Set the most local (server, protocol or default) binding of VAR to VAL.
255 The current binding of VAR is also set to VAL"
257 ((not (eq 'unbound (eudc-variable-server-value var)))
258 (eudc-server-set var val))
259 ((not (eq 'unbound (eudc-variable-protocol-value var)))
260 (eudc-protocol-set var val))
262 (eudc-default-set var val)))
265 (defun eudc-variable-default-value (var)
266 "Return the default binding of VAR.
267 Return `unbound' if VAR has no EUDC default value."
268 (let ((eudc-locals (get var 'eudc-locals)))
269 (if (and (boundp var)
271 (eudc-plist-get eudc-locals 'default 'unbound)
274 (defun eudc-variable-protocol-value (var &optional protocol)
275 "Return the value of VAR local to PROTOCOL.
276 Return `unbound' if VAR has no value local to PROTOCOL.
277 PROTOCOL defaults to `eudc-protocol'"
278 (let* ((eudc-locals (get var 'eudc-locals))
280 (if (not (and (boundp var)
282 (eudc-plist-member eudc-locals 'protocol)))
284 (setq protocol-locals (eudc-plist-get eudc-locals 'protocol))
285 (eudc-lax-plist-get protocol-locals
287 eudc-protocol) 'unbound))))
289 (defun eudc-variable-server-value (var &optional server)
290 "Return the value of VAR local to SERVER.
291 Return `unbound' if VAR has no value local to SERVER.
292 SERVER defaults to `eudc-server'"
293 (let* ((eudc-locals (get var 'eudc-locals))
295 (if (not (and (boundp var)
297 (eudc-plist-member eudc-locals 'server)))
299 (setq server-locals (eudc-plist-get eudc-locals 'server))
300 (eudc-lax-plist-get server-locals
302 eudc-server) 'unbound))))
304 (defun eudc-update-variable (var)
305 "Set the value of VAR according to its locals.
306 If the VAR has a server- or protocol-local value corresponding
307 to the current `eudc-server' and `eudc-protocol' then it is set
308 accordingly. Otherwise it is set to its EUDC default binding"
311 ((not (eq 'unbound (setq val (eudc-variable-server-value var))))
313 ((not (eq 'unbound (setq val (eudc-variable-protocol-value var))))
315 ((not (eq 'unbound (setq val (eudc-variable-default-value var))))
318 (defun eudc-update-local-variables ()
319 "Update all EUDC variables according to their local settings."
321 (mapcar 'eudc-update-variable eudc-local-vars))
323 (eudc-default-set 'eudc-query-function nil)
324 (eudc-default-set 'eudc-list-attributes-function nil)
325 (eudc-default-set 'eudc-protocol-attributes-translation-alist nil)
326 (eudc-default-set 'eudc-bbdb-conversion-alist nil)
327 (eudc-default-set 'eudc-switch-to-server-hook nil)
328 (eudc-default-set 'eudc-switch-from-server-hook nil)
329 (eudc-default-set 'eudc-protocol-has-default-query-attributes nil)
330 (eudc-default-set 'eudc-attribute-display-method-alist nil)
335 ;; Add PROTOCOL to the list of supported protocols
336 (defun eudc-register-protocol (protocol)
337 (unless (memq protocol eudc-supported-protocols)
338 (setq eudc-supported-protocols
339 (cons protocol eudc-supported-protocols))
340 (put 'eudc-protocol 'custom-type
341 `(choice :menu-tag "Protocol"
342 ,@(mapcar (lambda (s)
343 (list 'string ':tag (symbol-name s)))
344 eudc-supported-protocols))))
345 (or (memq protocol eudc-known-protocols)
346 (setq eudc-known-protocols
347 (cons protocol eudc-known-protocols))))
349 ;; Execute BODY restoring the server and protocol if anything goes wrong
350 (defmacro eudc-server-protect (&rest body)
351 `(let ((eudc-former-server eudc-server)
352 (eudc-former-protocol eudc-protocol))
353 (condition-case signal
356 (or (and (equal eudc-server eudc-former-server)
357 (equal eudc-protocol eudc-former-protocol))
358 (eudc-set-server eudc-former-server eudc-former-protocol t)))
359 (t (or (and (equal eudc-server eudc-former-server)
360 (equal eudc-protocol eudc-former-protocol))
361 (eudc-set-server eudc-former-server eudc-former-protocol t))
362 (signal (car signal) (cdr signal))))))
364 (defun eudc-echo-progress (server protocol)
365 (message "Querying %s with protocol %s..." server protocol))
367 (defun eudc-translate-query (query)
368 "Translate attribute names of QUERY.
369 The translation is done according to
370 `eudc-protocol-attributes-translation-alist'."
371 (if eudc-protocol-attributes-translation-alist
372 (mapcar '(lambda (attribute)
373 (let ((trans (assq (car attribute)
374 (symbol-value eudc-protocol-attributes-translation-alist))))
376 (cons (cdr trans) (cdr attribute))
381 (defun eudc-translate-attribute-list (list)
382 "Translate a list of attribute names LIST.
383 The translation is done according to
384 `eudc-protocol-attributes-translation-alist'."
385 (if eudc-protocol-attributes-translation-alist
387 (mapcar '(lambda (attribute)
388 (setq trans (assq attribute
389 (symbol-value eudc-protocol-attributes-translation-alist)))
396 (defun eudc-select (choices)
397 "Choose one from CHOICES using a completion buffer."
398 (setq eudc-pre-select-window-configuration (current-window-configuration))
399 (setq eudc-insertion-marker (point-marker))
400 (with-output-to-temp-buffer "*EUDC Completions*"
401 (apply 'display-completion-list
404 '(:activate-callback eudc-insert-selected)))))
406 (defun eudc-insert-selected (event extent user)
407 "Insert a completion at the appropriate point."
408 (when eudc-insertion-marker
409 (set-buffer (marker-buffer eudc-insertion-marker))
410 (goto-char eudc-insertion-marker)
411 (insert (extent-string extent)))
412 (if eudc-pre-select-window-configuration
413 (set-window-configuration eudc-pre-select-window-configuration))
414 (setq eudc-pre-select-window-configuration nil
415 eudc-insertion-marker nil))
417 (defun eudc-query (query &optional return-attributes)
418 "Query the current directory server with QUERY.
419 QUERY is a list of cons cells (ATTR . VALUE) where ATTR is an attribute
420 name and VALUE the corresponding value.
421 RETURN-ATTRIBUTES is a list of attributes to return defaulting to
422 `eudc-default-return-attributes'."
423 (unless eudc-query-function
424 (error "Don't know how to perform the query"))
425 (funcall eudc-query-function
426 (eudc-translate-query query)
429 (eudc-translate-attribute-list return-attributes))
430 ((listp eudc-default-return-attributes)
431 (eudc-translate-attribute-list eudc-default-return-attributes))
433 eudc-default-return-attributes))))
435 (defun eudc-multi-query (queries-or-words sentinel for-inline-expansion try-all)
436 "Query one or several directory servers.
437 The servers that are queried are determined by `eudc-multi-query-policy'
438 and `eudc-max-servers-to-query'.
439 If FOR-INLINE-EXPANSION is non-nil, the function is called for inline
440 expansion and QUERIES-OR-WORDS is expected to be a list of individual
441 words which are mapped onto `eudc-inline-query-format' to build the
442 directory queries. If FOR-INLINE-EXPANSION is nil, QUERIES-OR-WORDS is
443 expected to be a list of individual queries. An individual query is a
444 list of cons cells (ATTR . VALUE) where ATTR is an attribute name and
445 VALUE is the corresponding value.
446 SENTINEL is a symbol naming a sentinel function which is called
447 for each new server tried.
448 If TRY-ALL is non-nil then all servers are tried in order, otherwise
449 search stops when at leasta match is found on a server.
450 The returned value is a list of server matches. Each server match is
451 itself a list of the form ((SERVER .PROTO) MATCH1 MATCH2 ...) where
452 MATCHn is a matching record found on this server. A matching record is
453 a list of cons cells (ATTR . VALUE) where ATTR is an attribute name and
454 VALUE is the corresponding value"
455 (if (memq eudc-multi-query-policy
456 '(current-server server-then-hotlist))
458 (call-interactively 'eudc-set-server))
459 (or eudc-server-hotlist
460 (error "No server in the hotlist")))
461 (let (servers response found query-formats queries return-attributes)
462 ;; Prepare the list of servers to query
465 ((eq eudc-multi-query-policy 'hotlist)
467 ((eq eudc-multi-query-policy 'server-then-hotlist)
468 (cons (cons eudc-server eudc-protocol)
469 (delete (cons eudc-server eudc-protocol)
470 (copy-sequence eudc-server-hotlist))))
471 ((eq eudc-multi-query-policy 'current-server)
472 (list (cons eudc-server eudc-protocol)))
474 (error "Wrong value for `eudc-multi-query-policy': %S"
475 eudc-multi-query-policy))))
476 (if (and eudc-max-servers-to-query
477 (> (length servers) eudc-max-servers-to-query))
478 (setcdr (nthcdr (1- eudc-max-servers-to-query) servers) nil))
482 ;; Loop on the servers
484 (eudc-set-server (eudc-caar servers) (eudc-cdar servers) t)
488 (eudc-cdar servers)))
490 (if (null for-inline-expansion)
491 (setq queries queries-or-words
492 return-attributes nil)
493 ;; Determine which formats apply in the query-format list
495 (eudc-extract-n-word-formats eudc-inline-query-format
496 (length queries-or-words)))
500 (mapcar '(lambda (format)
501 (eudc-build-query-with-words-on-format
506 ;; Get the return attributes from the inline expansion format
507 (setq return-attributes (cdr eudc-inline-expansion-format)))
513 ;; If the query is a simple list of strings,
514 ;; try to build a valid query
515 (or (consp (car query))
516 (setq query (eudc-build-query-with-words-on-format query)))
517 (setq response (cons (cons (cons eudc-server eudc-protocol)
518 (eudc-query query return-attributes))
520 (setq found (or found (cadr (car response))))
521 (if (and found (null try-all))
522 (throw 'found response))))
524 (setq servers (cdr servers)))))
530 (defun eudc-format-attribute-name-for-display (attribute)
531 "Format a directory attribute name for display.
532 ATTRIBUTE is looked up in `eudc-user-attribute-names-alist' and replaced
533 by the corresponding user name if any. Otherwise it is capitalized and
534 underscore characters are replaced by spaces."
535 (let ((match (assq attribute eudc-user-attribute-names-alist)))
540 (split-string (symbol-name attribute) "_")
543 (defun eudc-print-attribute-value (field)
544 "Insert the value of the directory FIELD at point.
545 The directory attribute name in car of FIELD is looked up in
546 `eudc-attribute-display-method-alist' and the corresponding method,
547 if any, is called to print the value in cdr of FIELD."
548 (let ((match (assoc (downcase (car field))
549 eudc-attribute-display-method-alist))
550 (col (current-column))
554 (eval (list (cdr match) val))
560 (insert val-elem "\n")))
563 ((stringp val) (split-string val "\n"))
567 (defun eudc-print-record-field (field column-width)
568 "Print the record field FIELD.
569 FIELD is a list (ATTR VALUE1 VALUE2 ...) or cons-cell (ATTR . VAL)
570 COLUMN-WIDTH is the width of the first display column containing the
571 attribute name ATTR."
572 (let ((field-beg (point)))
573 ;; The record field that is passed to this function has already been processed
574 ;; by `eudc-format-attribute-name-for-display' so we don't need to call it
575 ;; again to display the attribute name
576 (insert (format (concat "%" (int-to-string column-width) "s: ")
578 (put-text-property field-beg (point) 'face 'bold)
579 (indent-to (+ 2 column-width))
580 (eudc-print-attribute-value field)))
582 (defun eudc-setup-record-display-buffer ()
583 "Setup a buffer to display records."
584 (let ((buffer (get-buffer-create "*Directory Query Results*"))
586 (switch-to-buffer buffer)
587 (setq buffer-read-only t
590 (insert "Directory Query Result\n")
591 (insert "======================\n\n\n")))
593 (defun eudc-close-record-display-buffer ()
594 "Insert the closing of a record display buffer."
595 (let ((inhibit-read-only t))
597 (widget-create 'push-button
598 :notify (lambda (&rest ignore)
602 (widget-create 'push-button
603 :notify (lambda (&rest ignore)
609 (defun eudc-display-records (records &optional raw-attr-names)
610 "Display RECORDS in a formatted buffer.
611 RECORDS is a server-prefixed record list as returned by `eudc-multi-query'.
612 If RAW-ATTR-NAMES is non-nil, the raw attribute names are displayed
613 otherwise they are formatted according to `eudc-user-attribute-names-alist'."
614 (let ((inhibit-read-only t)
622 ;; Print the number of records in the list
623 (setq num-matches (if (cdr records)
624 (length (cdr records))
626 (widget-insert (format "%d match%s found on %s (%s)\n\n"
628 (if (> num-matches 1) "es" "")
633 (setq server (car records))
634 ;; We need to switch servers because value display methods depend on the server
635 (eudc-set-server (car server) (cdr server) t)
636 ;; Drop server header from the record list
637 (setq records (cdr records))
638 ;; Replace field names with user names and compute max width
648 (symbol-name (car field))
649 (eudc-format-attribute-name-for-display (car field))))
650 (if (> (length attribute-name) width)
651 (setq width (length attribute-name)))
652 (cons attribute-name (cdr field))))
655 ;; Display the records
660 ;; Map over the record fields to print the attribute/value pairs
663 (eudc-print-record-field field width)))
665 ;; Store the record internal format in some convenient place
666 (overlay-put (make-overlay beg (point))
668 (cons server (car records)))
669 (setq records (cdr records))
673 (defun eudc-send-mail ()
674 "Send email to the address in the current record."
676 (unless (eq major-mode 'eudc-mode)
677 (error "This command should be called in EUDC buffers"))
679 (overlay-get (car (overlays-at (point))) 'eudc-record))
681 (unless record (error "Not on a record"))
682 ;; Since eudc-protocol-attributes-translation-alist is server-dependent we
683 ;; need to switch servers temporarily
685 (eudc-set-server (caar record) (cdar record) t)
686 (setq mail (cdr (assq
689 (symbol-value eudc-protocol-attributes-translation-alist)))
693 (funcall (get mail-user-agent 'composefunc) mail)
694 (error "Cannot determine email address"))))
696 (defun eudc-process-form ()
697 "Process the query form in current buffer and display the results."
698 (let (query value result inhibit-read-only)
699 (if (not (and (boundp 'eudc-form-widget-list)
700 eudc-form-widget-list))
701 (error "Not in a directory query form buffer")
704 (setq value (widget-value (cdr wid-field)))
705 (if (not (string= value ""))
706 (setq query (cons (cons (car wid-field) value)
708 eudc-form-widget-list)
709 (kill-buffer (current-buffer))
710 (setq result (eudc-multi-query (list query) 'eudc-echo-progress nil t))
711 ;; Display the query results
712 (eudc-setup-record-display-buffer)
714 (lambda (server-matches)
715 (eudc-display-records server-matches
716 eudc-use-raw-directory-names)))
718 (eudc-close-record-display-buffer))))
720 (defun eudc-filter-duplicate-attributes (record)
721 "Filter RECORD according to `eudc-duplicate-attribute-handling-method'."
727 ;; Search for multiple records
729 (not (listp (eudc-cdar rec))))
730 (setq rec (cdr rec)))
732 (if (null (eudc-cdar rec))
733 (list record) ; No duplicate attrs in this record
736 (if (listp (cdr field))
737 (setq duplicates (cons field duplicates))
738 (setq unique (cons field unique)))))
740 (setq result (list unique))
741 ;; Map over the record fields that have multiple values
745 (let ((method (if (consp eudc-duplicate-attribute-handling-method)
753 eudc-protocol-attributes-translation-alist)))
755 eudc-duplicate-attribute-handling-method))
756 eudc-duplicate-attribute-handling-method)))
758 ((or (null method) (eq 'list method))
760 (eudc-add-field-to-records field result)))
763 (eudc-add-field-to-records (cons (car field)
768 (eudc-add-field-to-records (cons (car field)
773 ((eq 'duplicate method)
775 (eudc-distribute-field-on-records field result)))))))
779 (defun eudc-filter-partial-records (records attrs)
780 "Eliminate records that do not contain all ATTRS from RECORDS."
789 (consp (assq attr rec))))
794 (defun eudc-add-field-to-records (field records)
795 "Add FIELD to each individual record in RECORDS and return the resulting list."
801 (defun eudc-distribute-field-on-records (field records)
802 "Duplicate each individual record in RECORDS according to value of FIELD.
803 Each copy is added a new field containing one of the values of FIELD."
805 (values (cdr field)))
806 ;; Uniquify values first
808 (setcdr values (delete (car values) (cdr values)))
809 (setq values (cdr values)))
813 (let ((result-list (copy-sequence records)))
814 (setq result-list (eudc-add-field-to-records
815 (cons (car field) value)
817 (setq result (append result-list result))
824 "Major mode used in buffers displaying the results of directory queries.
825 There is no sense in calling this command from a buffer other than
826 one containing the results of a directory query.
828 These are the special commands of EUDC mode:
829 q -- Kill this buffer.
830 f -- Display a form to query the current directory server.
831 n -- Move to next record.
832 p -- Move to previous record.
833 b -- Insert record at point into the BBDB database."
835 (kill-all-local-variables)
836 (setq major-mode 'eudc-mode)
837 (setq mode-name "EUDC")
838 (use-local-map eudc-mode-map)
840 (easy-menu-define eudc-emacs-menu eudc-mode-map "" (eudc-menu))
841 (setq mode-popup-menu (eudc-menu)))
842 (run-hooks 'eudc-mode-hook)
847 ;;{{{ High-level interfaces (interactive functions)
849 (defun eudc-customize ()
850 "Customize the EUDC package."
852 (customize-group 'eudc))
855 (defun eudc-set-server (server protocol &optional no-save)
856 "Set the directory server to SERVER using PROTOCOL.
857 Unless NO-SAVE is non-nil, the server is saved as the default
858 server for future sessions."
860 (read-from-minibuffer "Directory Server: ")
861 (intern (completing-read "Protocol: "
862 (mapcar '(lambda (elt)
863 (cons (symbol-name elt)
865 eudc-known-protocols)))))
866 (unless (or (member protocol
867 eudc-supported-protocols)
868 (load (concat "eudcb-" (symbol-name protocol)) t))
869 (error "Unsupported protocol: %s" protocol))
870 (run-hooks 'eudc-switch-from-server-hook)
871 (setq eudc-protocol protocol)
872 (setq eudc-server server)
873 (eudc-update-local-variables)
874 (run-hooks 'eudc-switch-to-server-hook)
876 (message "Current directory server is now %s (%s)" eudc-server eudc-protocol))
878 (eudc-save-options)))
881 (defun eudc-get-email (name)
882 "Get the email field of NAME from the directory server."
883 (interactive "sName: ")
885 (call-interactively 'eudc-set-server))
886 (let ((result (eudc-query (list (cons 'name name)) '(email)))
888 (if (null (cdr result))
889 (setq email (eudc-cdaar result))
890 (error "Multiple match. Use the query form"))
894 (error "No record matching %s" name)))
898 (defun eudc-get-phone (name)
899 "Get the phone field of NAME from the directory server."
900 (interactive "sName: ")
902 (call-interactively 'eudc-set-server))
903 (let ((result (eudc-query (list (cons 'name name)) '(phone)))
905 (if (null (cdr result))
906 (setq phone (eudc-cdaar result))
907 (error "Multiple match. Use the query form"))
911 (error "No record matching %s" name)))
914 (defun eudc-get-attribute-list ()
915 "Return a list of valid attributes for the current server.
916 When called interactively the list is formatted in a dedicated buffer
917 otherwise a list of symbols is returned."
919 (if eudc-list-attributes-function
920 (let ((entries (funcall eudc-list-attributes-function (interactive-p))))
923 (eudc-display-records entries t)
925 (error "The %s protocol has no support for listing attributes" eudc-protocol)))
927 (defun eudc-build-query-with-words-on-format (words &optional format)
928 "Use FORMAT to build a EUDC query from WORDS.
929 If FORMAT id nil, use the protocol default attribute name, if any."
935 (while (and words format)
936 (setq query-alist (cons (cons (car format) (car words))
938 (setq words (cdr words)
939 format (cdr format)))
940 ;; If the same attribute appears more than once, merge
941 ;; the corresponding values
942 (setq query-alist (nreverse query-alist))
944 (setq key (eudc-caar query-alist)
945 val (eudc-cdar query-alist)
946 cell (assq key query))
948 (setcdr cell (concat (cdr cell) " " val))
949 (setq query (cons (car query-alist) query)))
950 (setq query-alist (cdr query-alist)))
952 (if eudc-protocol-has-default-query-attributes
953 (mapconcat 'identity words " ")
954 (list (cons 'name (mapconcat 'identity words " ")))))))
956 (defun eudc-extract-n-word-formats (format-list n)
957 "Extract a list of N-long formats from FORMAT-LIST.
958 If none try N - 1 and so forth."
960 (while (and (null formats)
964 (mapcar '(lambda (format)
976 (defun eudc-expand-inline (&optional replace)
977 "Query the directory server, and expand the query string before point.
978 The query string consists of the buffer substring from the point back to
979 the preceding comma, colon or beginning of line.
980 The variable `eudc-inline-query-format' controls how to associate the
981 individual inline query words with directory attribute names.
982 After querying the server for the given string, the expansion specified by
983 `eudc-inline-expansion-format' is inserted in the buffer at point.
984 If REPLACE is non nil, then this expansion replaces the name in the buffer.
985 `eudc-expansion-overwrites-query' being non nil inverts the meaning of REPLACE.
986 Multiple servers can be tried with the same query until a match is found,
987 see `eudc-multi-query-policy'"
991 (if (re-search-backward "\\([:,]\\|^\\)[ \t]*"
996 (goto-char (match-end 0)))
998 (query-words (split-string (buffer-substring beg end) "[ \t]+"))
1004 (car (eudc-multi-query query-words nil t nil)))
1006 ;; Process response through eudc-inline-expansion-format. We need
1007 ;; to switch to the context of the server where a match was found
1009 (eudc-server-protect
1010 ;; Switch to the context of the server where a match was found
1011 (eudc-set-server (caar response) (cdar response) t)
1012 (setq response (cdr response)) ; Drop the server/protocol part
1014 ;; Loop over the matches found on the server
1016 (setq response-string (apply 'format
1017 (car eudc-inline-expansion-format)
1020 (or (cdr (assq field (car response)))
1022 (eudc-translate-attribute-list
1023 (cdr eudc-inline-expansion-format)))))
1024 (if (> (length response-string) 0)
1025 (setq response-strings
1026 (cons response-string response-strings)))
1027 (setq response (cdr response)))
1030 (and replace (not eudc-expansion-overwrites-query))
1031 (and (not replace) eudc-expansion-overwrites-query))
1032 (delete-region beg end))
1034 ((or (= (length response-strings) 1)
1035 (null eudc-multiple-match-handling-method)
1036 (eq eudc-multiple-match-handling-method 'first))
1037 (insert (car response-strings)))
1038 ((eq eudc-multiple-match-handling-method 'select)
1039 (eudc-select response-strings))
1040 ((eq eudc-multiple-match-handling-method 'all)
1041 (insert (mapconcat 'identity response-strings ", ")))
1042 ((eq eudc-multiple-match-handling-method 'abort)
1043 (error "There is more than one match for the query")))))))
1046 (defun eudc-query-form (&optional get-fields-from-server)
1047 "Display a form to query directory servers.
1048 If given a non-nil argument GET-FIELDS-FROM-SERVER, the function first
1049 queries the server for the existing fields and displays a corresponding form."
1051 (let ((fields (or (and get-fields-from-server
1052 (eudc-get-attribute-list))
1053 eudc-query-form-attributes))
1054 (buffer (get-buffer-create "*Directory Query Form*"))
1060 (switch-to-buffer buffer)
1061 (setq inhibit-read-only t)
1063 (kill-all-local-variables)
1064 (make-local-variable 'eudc-form-widget-list)
1065 (widget-insert "Directory Query Form\n")
1066 (widget-insert "====================\n\n")
1067 (widget-insert "Multi-query policy: "
1069 ((eq eudc-multi-query-policy 'current-server)
1070 "current server only\n")
1071 ((eq eudc-multi-query-policy 'hotlist)
1072 (if (or (null eudc-max-servers-to-query)
1073 (>= eudc-max-servers-to-query
1074 (length eudc-server-hotlist)))
1077 eudc-max-servers-to-query
1078 " servers of the hotlist\n")))
1079 ((eq eudc-multi-query-policy 'server-then-hotlist)
1080 (if (or (null eudc-max-servers-to-query)
1081 (> eudc-max-servers-to-query
1082 (length eudc-server-hotlist)))
1083 "current server then hotlist\n"
1084 (format "current server then first %d servers of the hotlist\n"
1085 (1- eudc-max-servers-to-query))))))
1086 (widget-insert "Current server is : " (or eudc-server
1088 (call-interactively 'eudc-set-server)
1091 (widget-insert "Protocol : " (symbol-name eudc-protocol) "\n")
1092 ;; Build the list of prompts
1093 (setq prompts (if eudc-use-raw-directory-names
1094 (mapcar 'symbol-name (eudc-translate-attribute-list fields))
1097 (or (and (assq field eudc-user-attribute-names-alist)
1098 (cdr (assq field eudc-user-attribute-names-alist)))
1099 (capitalize (symbol-name field)))))
1101 ;; Loop over prompt strings to find the longest one
1104 (if (> (length prompt) width)
1105 (setq width (length prompt)))))
1107 ;; Insert the first widget out of the mapcar to leave the cursor
1108 ;; in the first field
1109 (widget-insert "\n\n" (format (concat "%" (int-to-string width) "s: ") (car prompts)))
1111 (setq widget (widget-create 'editable-field :size 15))
1112 (setq eudc-form-widget-list (cons (cons (car fields) widget)
1113 eudc-form-widget-list))
1114 (setq fields (cdr fields))
1115 (setq prompts (cdr prompts))
1118 (widget-insert "\n\n" (format (concat "%" (int-to-string width) "s: ") (car prompts)))
1119 (setq widget (widget-create 'editable-field
1121 (setq eudc-form-widget-list (cons (cons field widget)
1122 eudc-form-widget-list))
1123 (setq prompts (cdr prompts))))
1125 (widget-insert "\n\n")
1126 (widget-create 'push-button
1127 :notify (lambda (&rest ignore)
1128 (eudc-process-form))
1131 (widget-create 'push-button
1132 :notify (lambda (&rest ignore)
1136 (widget-create 'push-button
1137 :notify (lambda (&rest ignore)
1141 (use-local-map widget-keymap)
1145 (defun eudc-bookmark-server (server protocol)
1146 "Add SERVER using PROTOCOL to the EUDC `servers' hotlist."
1147 (interactive "sDirectory server: \nsProtocol: ")
1148 (if (member (cons server protocol) eudc-server-hotlist)
1149 (error "%s:%s is already in the hotlist" protocol server)
1150 (setq eudc-server-hotlist (cons (cons server protocol) eudc-server-hotlist))
1152 (eudc-save-options)))
1154 (defun eudc-bookmark-current-server ()
1155 "Add current server to the EUDC `servers' hotlist."
1157 (eudc-bookmark-server eudc-server eudc-protocol))
1159 (defun eudc-save-options ()
1160 "Save options to `eudc-options-file'."
1163 (set-buffer (find-file-noselect eudc-options-file t))
1164 (goto-char (point-min))
1165 ;; delete the previous setq
1166 (let ((standard-output (current-buffer))
1172 (let ((sexp (condition-case nil
1173 (read (current-buffer))
1174 (end-of-file (throw 'found nil)))))
1177 ((eq (car sexp) 'eudc-set-server)
1178 (delete-region (save-excursion
1182 (setq set-server-p t))
1183 ((and (eq (car sexp) 'setq)
1184 (eq (eudc-cadr sexp) 'eudc-server-hotlist))
1185 (delete-region (save-excursion
1189 (setq set-hotlist-p t))
1190 ((and (eq (car sexp) 'provide)
1191 (equal (eudc-cadr sexp) '(quote eudc-options-file)))
1192 (setq provide-p t)))
1196 (throw 'found t))))))
1197 (if (eq (point-min) (point-max))
1198 (princ ";; This file was automatically generated by eudc.el.\n\n"))
1200 (princ "(provide 'eudc-options-file)\n"))
1203 (delete-blank-lines)
1204 (princ "(eudc-set-server ")
1207 (prin1 eudc-protocol)
1209 (princ "(setq eudc-server-hotlist '")
1210 (prin1 eudc-server-hotlist)
1214 (defun eudc-move-to-next-record ()
1215 "Move to next record, in a buffer displaying directory query results."
1217 (if (not (eq major-mode 'eudc-mode))
1218 (error "Not in a EUDC buffer")
1219 (let ((pt (next-overlay-change (point))))
1220 (if (< pt (point-max))
1222 (error "No more records after point")))))
1224 (defun eudc-move-to-previous-record ()
1225 "Move to previous record, in a buffer displaying directory query results."
1227 (if (not (eq major-mode 'eudc-mode))
1228 (error "Not in a EUDC buffer")
1229 (let ((pt (previous-overlay-change (point))))
1230 (if (> pt (point-min))
1232 (error "No more records before point")))))
1238 ;;{{{ Menus an keymaps
1243 (let ((map (make-sparse-keymap)))
1244 (define-key map "q" 'kill-this-buffer)
1245 (define-key map "x" 'kill-this-buffer)
1246 (define-key map "f" 'eudc-query-form)
1247 (define-key map "b" 'eudc-try-bbdb-insert)
1248 (define-key map "n" 'eudc-move-to-next-record)
1249 (define-key map "p" 'eudc-move-to-previous-record)
1251 (set-keymap-parent eudc-mode-map widget-keymap)
1253 (defconst eudc-custom-generated-menu (and (featurep 'menubar)
1254 (cdr (custom-menu-create 'eudc))))
1256 (defconst eudc-tail-menu
1258 ["Query with Form" eudc-query-form t]
1259 ["Expand Inline Query" eudc-expand-inline t]
1260 ["Send mail to this address" eudc-send-mail
1261 (and (overlays-at (point))
1262 (overlay-get (car (overlays-at (point))) 'eudc-record))]
1263 ["Insert Record into BBDB" eudc-insert-record-at-point-into-bbdb
1264 (and (or (featurep 'bbdb)
1265 (prog1 (locate-library "bbdb") (message "")))
1266 (overlays-at (point))
1267 (overlay-get (car (overlays-at (point))) 'eudc-record))]
1268 ["Insert All Records into BBDB" eudc-batch-export-records-to-bbdb
1269 (and (eq major-mode 'eudc-mode)
1270 (or (featurep 'bbdb)
1271 (prog1 (locate-library "bbdb") (message ""))))]
1273 ["Get Email" eudc-get-email t]
1274 ["Get Phone" eudc-get-phone t]
1275 ["List Valid Attribute Names" eudc-get-attribute-list t]
1277 ,(cons "Customize" eudc-custom-generated-menu)))
1280 (defconst eudc-server-menu
1282 ["Bookmark Current Server" eudc-bookmark-current-server t]
1283 ["Edit Server List" eudc-edit-hotlist t]
1284 ["New Server" eudc-set-server t]))
1288 (append '("Directory Search")
1295 (let* ((server (car servspec))
1296 (protocol (cdr servspec))
1297 (proto-name (symbol-name protocol)))
1298 (setq command (intern (concat "eudc-set-server-"
1302 (if (not (fboundp command))
1306 (eudc-set-server ,server (quote ,protocol))
1307 (message "Selected directory server is now %s (%s)"
1310 (vector (format "%s (%s)" server proto-name)
1313 :selected `(equal eudc-server ,server)))))
1314 eudc-server-hotlist)
1318 (defun eudc-install-menu ()
1320 ((and eudc-xemacs-p (featurep 'menubar))
1321 (add-submenu '("Tools") (eudc-menu)))
1324 ((fboundp 'easy-menu-add-item)
1325 (let ((menu (eudc-menu)))
1326 (easy-menu-add-item nil '("tools") (easy-menu-create-menu (car menu)
1328 ((fboundp 'easy-menu-create-keymaps)
1329 (easy-menu-define eudc-menu-map eudc-mode-map "Directory Client Menu" (eudc-menu))
1332 [menu-bar tools eudc]
1333 (cons "Directory Search"
1334 (easy-menu-create-keymaps "Directory Search" (cdr (eudc-menu))))))
1336 (error "Unknown version of easymenu"))))
1340 ;;; Load time initializations :
1342 ;;; Load the options file
1343 (if (and (not noninteractive)
1344 (and (locate-library eudc-options-file)
1345 (message "")) ; Remove modeline message
1346 (not (featurep 'eudc-options-file)))
1347 (load eudc-options-file))
1349 ;;; Install the full menu
1350 (unless (or (featurep 'infodock) noninteractive)
1351 (eudc-install-menu))
1353 ;;; The following installs a short menu for EUDC at XEmacs startup.
1356 (defun eudc-load-eudc ()
1357 "Load the Emacs Unified Directory Client.
1358 This does nothing except loading eudc by autoload side-effect."
1365 (cond ((not (string-match "XEmacs" emacs-version))
1366 (defvar eudc-tools-menu (make-sparse-keymap "Directory Search"))
1367 (fset 'eudc-tools-menu (symbol-value 'eudc-tools-menu))
1369 (define-key eudc-tools-menu [phone]
1370 '("Get Phone" . eudc-get-phone))
1371 (define-key eudc-tools-menu [email]
1372 '("Get Email" . eudc-get-email))
1373 (define-key eudc-tools-menu [separator-eudc-email]
1375 (define-key eudc-tools-menu [expand-inline]
1376 '("Expand Inline Query" . eudc-expand-inline))
1377 (define-key eudc-tools-menu [query]
1378 '("Query with Form" . eudc-query-form))
1379 (define-key eudc-tools-menu [separator-eudc-query]
1381 (define-key eudc-tools-menu [new]
1382 '("New Server" . eudc-set-server))
1383 (define-key eudc-tools-menu [load]
1384 '("Load Hotlist of Servers" . eudc-load-eudc)))
1387 (let ((menu '("Directory Search"
1388 ["Load Hotlist of Servers" eudc-load-eudc t]
1389 ["New Server" eudc-set-server t]
1391 ["Query with Form" eudc-query-form t]
1392 ["Expand Inline Query" eudc-expand-inline t]
1394 ["Get Email" eudc-get-email t]
1395 ["Get Phone" eudc-get-phone t])))
1396 (if (not (featurep 'eudc-autoloads))
1397 (if (string-match "XEmacs" emacs-version)
1398 (if (and (featurep 'menubar)
1399 (not (featurep 'infodock))
1400 (not noninteractive))
1401 (add-submenu '("Tools") menu))
1404 ((fboundp 'easy-menu-add-item)
1405 (easy-menu-add-item nil '("tools")
1406 (easy-menu-create-menu (car menu)
1408 ((fboundp 'easy-menu-create-keymaps)
1411 [menu-bar tools eudc]
1412 (cons "Directory Search"
1413 (easy-menu-create-keymaps "Directory Search"
1414 (cdr menu)))))))))))
1420 ;;; eudc.el ends here