(hexlify-buffer, dehexlify-buffer):
[bpt/emacs.git] / lisp / ph.el
CommitLineData
43a51f78
RS
1;;; ph.el --- Client for the CCSO directory system (aka PH/QI)
2
3;; Copyright (C) 1997 Free Software Foundation, Inc.
4
5;; Author: Oscar Figueiredo <Oscar.Figueiredo@di.epfl.ch>
6;; Maintainer: Oscar Figueiredo <Oscar.Figueiredo@di.epfl.ch>
7;; Created: May 1997
9fb87caf 8;; Version: 2.8
43a51f78
RS
9;; Keywords: help
10
11;; This file is part of GNU Emacs
12
13;; GNU Emacs 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)
16;; any later version.
17
18;; GNU Emacs 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.
22
23;; You should have received a copy of the GNU General Public License
24;; along with GNU Emacs; 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.
27
28;;; Commentary:
29;; This package provides functions to query CCSO PH/QI nameservers
30;; through an interactive form or replace inline query strings in
31;; buffers with appropriately formatted query results (especially
9fb87caf 32;; used to expand email addresses in message buffers). It also
43a51f78
RS
33;; interfaces with the BBDB package to let you register entries of
34;; the CCSO PH/QI directory into your own database. The CCSO PH/QI
35;; white pages system was developped at UIUC and is in use in more
9fb87caf 36;; than 300 sites in the world. The distribution can be found at
43a51f78
RS
37;; ftp://uiarchive.cso.uiuc.edu/pub/packages/ph Traditionally the
38;; server is called QI while the client is called PH.
39
40;;; Installation:
9fb87caf 41;; This package uses the custom and widget libraries. If they are not already
43a51f78
RS
42;; installed on your system get them from http://www.dina.kvl.dk/~abraham/custom/
43;; Then uncomment and add the following to your .emacs file:
44;; (require 'ph)
45;; (eval-after-load "message"
46;; '(define-key message-mode-map [(control ?c) (tab)] 'ph-expand-inline))
47;; (eval-after-load "mail"
48;; '(define-key mail-mode-map [(control ?c) (tab)] 'ph-expand-inline))
49;; See the info file for details
50
51;; This package runs under XEmacs 19.15 or 20 and under Emacs 19.34 and above
52
53;;; Usage:
54;; - Provided you did the installation as proposed in the above section,
55;; inline expansion will be available when you compose an email
9fb87caf 56;; message. Type the name of somebody recorded in your PH/QI server and hit
43a51f78
RS
57;; C-c TAB, this will overwrite the name with the corresponding email
58;; address
59;; - M-x ph-customize to customize inline expansion and other features to
60;; your needs.
61;; - Look for the Ph submenu in the Tools menu for more.
62;; See the info file for details.
63
64;;; Code:
65
66(require 'wid-edit)
67
68(if (not (fboundp 'make-overlay))
69 (require 'overlay))
70
71(autoload 'custom-menu-create "cus-edit")
72
73;;{{{ Package customization variables
74
75(defgroup ph nil
76 "CCSO (PH/QI) directory system client"
77 :group 'mail
78 :group 'comm)
79
80(defcustom ph-server nil
81 "*The name or IP address of the CCSO (PH/QI) server.
82A port number may be specified by appending a colon and a
83number to the name of the server."
84 :type '(string :tag "Server")
85 :group 'ph)
86
87(defcustom ph-strict-return-matches t
9fb87caf 88 "*If non-nil, entries not containing all requested return fields are ignored."
43a51f78
RS
89 :type 'boolean
90 :group 'ph)
91
92(defcustom ph-default-return-fields nil
93 "*A list of the default fields to extract from CCSO entries.
94If it contains `all' then all available fields are returned.
95nil means return the default fields as configured in the server."
96 :type '(repeat (symbol :tag "Field name"))
97 :group 'ph)
98
99(defcustom ph-multiple-match-handling-method 'select
9fb87caf 100 "*What to do when multiple entries match an inline expansion query.
43a51f78 101Possible values are:
9fb87caf
RS
102`first' (equivalent to nil) which means consider the first match,
103`select' pop-up a selection buffer,
104`all' use all matches,
105`abort' the operation is aborted, an error is signaled."
43a51f78
RS
106 :type '(choice :menu-tag "Method"
107 (const :menu-tag "First" first)
108 (const :menu-tag "Select" select)
109 (const :menu-tag "All" all)
110 (const :menu-tag "Abort" abort)
111 (const :menu-tag "None" nil))
112 :group 'ph)
113
114(defcustom ph-duplicate-fields-handling-method '((email . duplicate))
115 "*A method to handle entries containing duplicate fields.
116This is either an alist (FIELD . METHOD) or a symbol METHOD.
117The alist form of the variable associates a method to an individual field,
118the second form specifies a method applicable to all fields.
119Available methods are:
9fb87caf 120`list' or nil lets the value of the field be a list of values,
43a51f78
RS
121`first' keeps the first value and discards the others,
122`concat' concatenates the values into a single multiline string,
123`duplicate' duplicates the entire entry into as many instances as
124different values."
125 :type '(choice (const :menu-tag "List" list)
126 (const :menu-tag "First" first)
127 (const :menu-tag "Concat" concat)
128 (const :menu-tag "Duplicate" duplicate)
129 (repeat :menu-tag "Per Field Specification"
130 :tag "Per Field Specification"
131 (cons :tag "Field/Method"
132 :value (nil . list)
133 (symbol :tag "Field name")
134 (choice :tag "Method"
135 :menu-tag "Method"
136 (const :menu-tag "List" list)
137 (const :menu-tag "First" first)
138 (const :menu-tag "Concat" concat)
139 (const :menu-tag "Duplicate" duplicate)))))
140 :group 'ph
141 )
142
143(defcustom ph-inline-query-format-list nil
144 "*Format of an inline expansion query.
145If the inline query string consists of several words, this list specifies
146how these individual words are associated to CCSO database field names.
147If nil all the words will be mapped onto the default CCSO database key."
148 :type '(repeat (symbol :tag "Field name"))
149 :group 'ph)
150
151(defcustom ph-expanding-overwrites-query t
9fb87caf 152 "*If non nil, expanding a query overwrites the query string."
43a51f78
RS
153 :type 'boolean
154 :group 'ph)
155
156(defcustom ph-inline-expansion-format '("%s" email)
157 "*A list specifying the format of the expansion of inline queries.
9fb87caf
RS
158This variable controls what `ph-expand-inline' actually inserts in the buffer.
159First element is a string passed to `format'. Remaining elements are symbols
43a51f78 160indicating CCSO database field names, corresponding field values are passed
9fb87caf 161as additional arguments to `format'."
43a51f78
RS
162 :type '(list (string :tag "Format String")
163 (repeat :inline t
164 :tag "Field names"
165 (symbol :tag "")))
166 :group 'ph)
167
168(defcustom ph-form-fields '(name email phone)
169 "*A list of fields presented in the query form."
170 :tag "Default Fields in Query Forms"
171 :type '(repeat (symbol :tag "Field name"))
172 :group 'ph)
173
174(defcustom ph-fieldname-formstring-alist '((url . "URL")
175 (callsign . "HAM Call Sign")
176 (id . "ID")
177 (email . "E-Mail")
178 (firstname . "First Name"))
9fb87caf
RS
179 "*Map CCSO database field names into prompt strings for query/response.
180Prompt strings for fields that are not listed here
181are derived by splitting the field name
43a51f78
RS
182at `_' signs and capitalizing the individual words."
183 :tag "Mapping of Field Names onto Prompt Strings"
184 :type '(repeat (cons :tag "Field"
185 (symbol :tag "Name")
186 (string :tag "Prompt string")))
187 :group 'ph)
188
189(defcustom ph-bbdb-conversion-alist
190 '((name . name)
191 (net . email)
192 (address . (ph-bbdbify-address address "Address"))
193 (phone . ((ph-bbdbify-phone phone "Phone")
194 (ph-bbdbify-phone office_phone "Office Phone"))))
195 "*A mapping from BBDB to PH/QI fields.
196This is a list of cons cells (BBDB-FIELD . SPEC-OR-LIST) where
197BBDB-FIELD is the name of a field that must be defined in your BBDB
198environment (standard field names are `name', `company', `net', `phone',
199`address' and `notes'). SPEC-OR-LIST is either a single SPEC or a list
9fb87caf 200of SPECs. Lists of specs are valid only for the `phone' and `address'
43a51f78 201BBDB fields. SPECs are sexps which are evaluated:
9fb87caf
RS
202 a string evaluates to itself,
203 a symbol evaluates to the symbol value. Symbols naming PH/QI fields
204 present in the record evaluate to the value of the field in the record,
205 a form is evaluated as a function. The argument list may contain PH/QI
43a51f78 206 field names which eval to the corresponding values in the
9fb87caf
RS
207 record. The form evaluation should return something appropriate for
208 the particular BBDB-FIELD (see `bbdb-create-internal').
209 `ph-bbdbify-phone' and `ph-bbdbify-address' are provided as convenience
43a51f78
RS
210 functions to parse phones and addresses."
211 :tag "BBDB to CCSO Field Name Mapping"
212 :type '(repeat (cons :tag "Field Name"
213 (symbol :tag "BBDB Field")
214 (sexp :tag "Conversion Spec")))
215 :group 'ph)
216
217(defcustom ph-options-file "~/.ph-options"
9fb87caf 218 "*A file where the PH `servers' hotlist is stored."
43a51f78
RS
219 :type '(file :Tag "File Name:"))
220
221(defcustom ph-mode-hook nil
9fb87caf 222 "*Normal hook run on entry to PH mode."
43a51f78
RS
223 :type '(repeat (sexp :tag "Hook")))
224
225;;}}}
226
227
228;;{{{ Internal cooking
229
230
231(defconst ph-xemacs-p (string-match "XEmacs" emacs-version))
232(defconst ph-emacs-p (not ph-xemacs-p))
233(defconst ph-xemacs-mule-p (and ph-xemacs-p
234 (featurep 'mule)))
235(defconst ph-emacs-mule-p (and ph-emacs-p
236 (featurep 'mule)))
237
238(defvar ph-server-hotlist nil)
239
240(defconst ph-default-server-port 105
9fb87caf 241 "Default TCP port for CCSO directory services.")
43a51f78
RS
242
243(defvar ph-form-widget-list nil)
244(defvar ph-process-buffer nil)
245(defvar ph-read-point)
246
247;;; Load the options file
248(if (and (and (locate-library ph-options-file)
249 (message "")) ; Remove modeline message
250 (not (featurep 'ph-options-file)))
251 (load ph-options-file))
252
e865033e
RS
253(defun ph-cadr (obj)
254 (car (cadr obj)))
255
256(defun ph-cdar (obj)
257 (cdr (car obj)))
258
43a51f78
RS
259(defun ph-mode ()
260 "Major mode used in buffers displaying the results of PH queries.
261There is no sense in calling this command from a buffer other than
262one containing the results of a PH query.
263
9fb87caf
RS
264These are the special commands of PH mode:
265 q -- kill this buffer.
266 f -- Display a form to query the CCSO PH/QI nameserver.
267 n -- Move to next record.
268 p -- Move to previous record."
43a51f78
RS
269 (interactive)
270 (kill-all-local-variables)
271 (setq major-mode 'ph-mode)
272 (setq mode-name "PH")
273 (use-local-map ph-mode-map)
274 (setq mode-popup-menu (ph-menu))
275 (run-hooks 'ph-mode-hook)
276 )
277
278(defun ph-display-records (records &optional raw-field-names)
279 "Display the record list RECORDS in a formatted buffer.
9fb87caf
RS
280If RAW-FIELD-NAMES is non-nil, the raw field names are displayed
281otherwise they are formatted according to `ph-fieldname-formstring-alist'."
43a51f78
RS
282 (let ((buffer (get-buffer-create "*PH Query Results*"))
283 inhibit-read-only
284 precords
285 (width 0)
286 beg field-beg
287 field-name)
288 (switch-to-buffer buffer)
289 (setq buffer-read-only t)
290 (setq inhibit-read-only t)
291 (erase-buffer)
292 (insert "PH Query Result\n")
293 (insert "===============\n\n\n")
294 (if (null records)
295 (insert "No match found.\n"
296 (if ph-strict-return-matches
297 "Try setting ph-strict-return-matches to nil or change ph-default-return-fields."
298 ""))
299 ;; Replace field names with prompt strings, compute prompt max width
300 (setq precords
301 (mapcar
302 (function
303 (lambda (record)
304 (mapcar
305 (function
306 (lambda (field)
307 (setq field-name (if raw-field-names
308 (symbol-name (car field))
309 (or (and (assq (car field) ph-fieldname-formstring-alist)
310 (cdr (assq (car field) ph-fieldname-formstring-alist)))
311 (capitalize (mapconcat '(lambda (char)
312 (if (eq char ?_)
313 " "
314 (char-to-string char)))
315 (symbol-name (car field))
316 "")))))
317 (if (> (length field-name) width)
318 (setq width (length field-name)))
319 (cons field-name (cdr field))))
320 record)))
321 records))
322 (mapcar (function
323 (lambda (record)
324 (setq beg (point))
325 ;; Actually insert the field/value pairs
326 (mapcar (function
327 (lambda (field)
328 (setq field-beg (point))
329 (insert (format (concat "%" width "s: ") (car field)))
330 (put-text-property field-beg (point) 'face 'bold)
331 (mapcar (function
332 (lambda (val)
333 (indent-to (+ 2 width))
334 (insert val "\n")))
335 (if (stringp (cdr field))
336 (split-string (cdr field) "\n")
337 (cdr field)))))
338 record)
339 ;; Store the record internal format in some convenient place
340 (overlay-put (make-overlay beg (point))
341 'ph-record
342 (car records))
343 (setq records (cdr records))
344 (insert "\n")))
345 precords))
346 (insert "\n")
347 (widget-create 'push-button
348 :notify (lambda (&rest ignore)
349 (ph-query-form))
350 "New query")
351 (widget-insert " ")
352 (widget-create 'push-button
353 :notify (lambda (&rest ignore)
354 (kill-this-buffer))
355 "Quit")
356 (ph-mode)
357 (widget-setup)
358 )
359 )
360
361(defun ph-process-form ()
9fb87caf 362 "Process the form in current buffer and display the results."
43a51f78
RS
363 (let (query-alist
364 value)
365 (if (not (and (boundp 'ph-form-widget-list)
366 ph-form-widget-list))
367 (error "Not in a PH query form buffer")
368 (mapcar (function
369 (lambda (wid-field)
370 (setq value (widget-value (cdr wid-field)))
371 (if (not (string= value ""))
372 (setq query-alist (cons (cons (car wid-field) value)
373 query-alist)))))
374 ph-form-widget-list)
375 (kill-buffer (current-buffer))
376 (ph-display-records (ph-query-internal query-alist))
377 )))
378
379
380(defun ph-query-internal (query &optional return-fields)
381 "Query the PH/QI server with QUERY.
382QUERY can be a string NAME or a list made of strings NAME
383and/or cons cells (KEY . VALUE) where KEYs should be valid
9fb87caf
RS
384CCSO database keys. NAME is equivalent to (DEFAULT . NAME),
385where DEFAULT is the default key of the database.
386RETURN-FIELDS is a list of database fields to return,
387defaulting to `ph-default-return-fields'."
43a51f78
RS
388 (let (request)
389 (if (null return-fields)
390 (setq return-fields ph-default-return-fields))
391 (setq request
392 (concat "query "
393 (if (stringp query)
394 query
395 (mapconcat (function (lambda (elt)
396 (if (stringp elt) elt)
397 (format "%s=%s" (car elt) (cdr elt))))
398 query
399 " "))
400 (if return-fields
401 (concat " return " (mapconcat 'symbol-name return-fields " ")))))
402 (and (> (length request) 6)
403 (ph-do-request request)
404 (ph-parse-query-result return-fields))))
405
406(defun ph-parse-query-result (&optional fields)
9fb87caf 407 "Return a list of alists of key/values from in `ph-process-buffer'.
43a51f78 408Fields not in FIELDS are discarded."
9fb87caf
RS
409 (let (record
410 records
411 line-regexp
412 current-key
413 key
414 value
415 ignore)
43a51f78
RS
416 (save-excursion
417 (message "Parsing results...")
418 (set-buffer ph-process-buffer)
419 (goto-char (point-min))
420 (while (re-search-forward "^\\(-[0-9]+\\):\\([0-9]+\\):" nil t)
421 (catch 'ignore
422 (setq line-regexp (concat "^\\(-[0-9]+\\):" (match-string 2) ":[ \t]*\\([-a-zA-Z_]*\\)?:[ \t]*\\(.*\\)$"))
423 (beginning-of-line)
424 (setq record nil
425 ignore nil
426 current-key nil)
427 (while (re-search-forward line-regexp nil t)
428 (catch 'skip-line
429 (if (string= "-508" (match-string 1))
9fb87caf 430 ;; A field is missing in this entry. Skip it or skip the
43a51f78
RS
431 ;; whole record (see ph-strict-return-matches)
432 (if (not ph-strict-return-matches)
433 (throw 'skip-line t)
434 (while (re-search-forward line-regexp nil t))
435 (setq ignore t)
436 (throw 'ignore t)))
437 (setq key (and (not (string= (match-string 2) ""))
438 (intern (match-string 2)))
439 value (match-string 3))
440 (if (and current-key
441 (eq key current-key))
442 (setq key nil)
443 (setq current-key key))
444 (if (or (null fields)
445 (memq 'all fields)
446 (memq current-key fields))
447 (if key
448 (setq record (cons (cons key value) record)) ; New key
e865033e
RS
449 (setcdr (car record) (if (listp (ph-cdar record))
450 (append (ph-cdar record) (list value))
451 (list (ph-cdar record) value))))))))
43a51f78
RS
452 (and (not ignore)
453 (or (null fields)
454 (memq 'all fields)
455 (setq record (nreverse record)))
456 (setq record (if (not (eq 'list ph-duplicate-fields-handling-method))
457 (ph-filter-duplicate-fields record)
458 (list record)))
459 (setq records (append record records))))
460 )
461 (message "Done")
462 records)
463 )
464
465(defun ph-filter-duplicate-fields (record)
9fb87caf 466 "Filter RECORD according to `ph-duplicate-fields-handling-method'."
43a51f78
RS
467 (let ((rec record)
468 unique
469 duplicates
470 result)
471
472 ;; Search for multiple records
473 (while (and rec
e865033e 474 (not (listp (ph-cdar rec))))
43a51f78
RS
475 (setq rec (cdr rec)))
476
e865033e 477 (if (null (ph-cdar rec))
43a51f78
RS
478 (list record) ; No duplicate fields in this record
479 (mapcar (function
480 (lambda (field)
481 (if (listp (cdr field))
482 (setq duplicates (cons field duplicates))
483 (setq unique (cons field unique)))))
484 record)
485 (setq result (list unique))
486 (mapcar (function
487 (lambda (field)
488 (let ((method (if (consp ph-duplicate-fields-handling-method)
489 (cdr (assq (car field) ph-duplicate-fields-handling-method))
490 ph-duplicate-fields-handling-method)))
491 (cond
492 ((or (null method) (eq 'list method))
493 (setq result
494 (ph-add-field-to-records field result)))
495 ((eq 'first method)
496 (setq result
e865033e 497 (ph-add-field-to-records (cons (car field) (ph-cadr field)) result)))
43a51f78
RS
498 ((eq 'concat method)
499 (setq result
500 (ph-add-field-to-records (cons (car field)
501 (mapconcat
502 'identity
503 (cdr field)
504 "\n")) result)))
505 ((eq 'duplicate method)
506 (setq result
507 (ph-distribute-field-on-records field result)))))))
508 duplicates)
509 result)))
510
511(defun ph-add-field-to-records (field records)
512 "Add FIELD to each individual record in RECORDS and return the resulting list."
513 (mapcar (function
514 (lambda (r)
515 (cons field r)))
516 records))
517
518(defun ph-distribute-field-on-records (field records)
519 "Duplicate each individual record in RECORDS according to value of FIELD.
520Each copy is added a new field containing one of the values of FIELD."
521 (let (result
522 (values (cdr field)))
523 ;; Uniquify values first
524 (while values
525 (setcdr values (delete (car values) (cdr values)))
526 (setq values (cdr values)))
527 (mapcar (function
528 (lambda (value)
529 (let ((result-list (copy-sequence records)))
530 (setq result-list (ph-add-field-to-records (cons (car field) value)
531 result-list))
532 (setq result (append result-list result))
533 )))
534 (cdr field))
535 result)
536 )
537
538(defun ph-do-request (request)
539 "Send REQUEST to the server.
540Wait for response and return the buffer containing it."
541 (let (process
542 buffer)
543 (unwind-protect
544 (progn
545 (message "Contacting server...")
546 (setq process (ph-open-session))
547 (if process
548 (save-excursion
549 (set-buffer (setq buffer (process-buffer process)))
550 (ph-send-command process request)
551 (message "Request sent, waiting for reply...")
552 (ph-read-response process))))
553 (if process
554 (ph-close-session process)))
555 buffer))
556
557(defun ph-open-session (&optional server)
558 "Open a connection to the given CCSO SERVER.
559SERVER is either a string naming the server or a list (NAME PORT)."
560 (let (process
561 host
562 port)
563 (catch 'done
564 (if (null server)
565 (setq server (or ph-server
566 (call-interactively 'ph-set-server))))
567 (string-match "\\(.*\\)\\(:\\(.*\\)\\)?" server)
568 (setq host (match-string 1 server))
569 (setq port (or (match-string 3 server)
570 ph-default-server-port))
571 (setq ph-process-buffer (get-buffer-create (format " *PH-%s*" host)))
572 (save-excursion
573 (set-buffer ph-process-buffer)
574 (erase-buffer)
575 (setq ph-read-point (point))
576 (and ph-xemacs-mule-p
577 (set-buffer-file-coding-system 'binary t)))
578 (setq process (open-network-stream "ph" ph-process-buffer host port))
579 (if (null process)
580 (throw 'done nil))
581 (process-kill-without-query process)
582 process)))
583
584
585(defun ph-close-session (process)
586 (save-excursion
587 (set-buffer (process-buffer process))
588 (ph-send-command process "quit")
589 (ph-read-response process)
590 (if (fboundp 'add-async-timeout)
591 (add-async-timeout 10 'delete-process process)
592 (run-at-time 2 nil 'delete-process process))))
593
594(defun ph-send-command (process command)
595 (goto-char (point-max))
596 (process-send-string process command)
597 (process-send-string process "\r\n")
598 )
599
600(defun ph-read-response (process &optional return-response)
601 "Read a response from the PH/QI query process PROCESS.
9fb87caf 602Returns nil if response starts with an error code. If the
43a51f78 603response is successful the return code or the reponse itself is returned
9fb87caf 604depending on RETURN-RESPONSE."
43a51f78
RS
605 (let ((case-fold-search nil)
606 return-code
607 match-end)
608 (goto-char ph-read-point)
609 ;; CCSO protocol : response complete if status >= 200
610 (while (not (re-search-forward "^\\(^[2-5].*\\):.*\n" nil t))
611 (accept-process-output process)
612 (goto-char ph-read-point))
613 (setq match-end (point))
614 (goto-char ph-read-point)
615 (if (and (setq return-code (match-string 1))
616 (setq return-code (string-to-number return-code))
617 (>= (abs return-code) 300))
618 (progn (setq ph-read-point match-end) nil)
619 (setq ph-read-point match-end)
620 (if return-response
621 (buffer-substring (point) match-end)
622 return-code))))
623
624(defun ph-create-bbdb-record (record)
625 "Create a BBDB record using the RECORD alist.
626RECORD is an alist of (KEY . VALUE) where KEY is a symbol naming a field
9fb87caf 627of the PH/QI database and VALUE is the corresponding value for the record."
43a51f78
RS
628 ;; This function runs in a special context where lisp symbols corresponding
629 ;; to field names in record are bound to the corresponding values
630 (eval
631 `(let* (,@(mapcar '(lambda (c)
632 (list (car c) (if (listp (cdr c))
633 (list 'quote (cdr c))
634 (cdr c))))
635 record)
636 bbdb-name
637 bbdb-company
638 bbdb-net
639 bbdb-address
640 bbdb-phones
641 bbdb-notes
642 spec
643 bbdb-record
644 value)
645
646 ;; BBDB standard fields
647 (setq bbdb-name (ph-parse-spec (cdr (assq 'name ph-bbdb-conversion-alist)) record nil)
648 bbdb-company (ph-parse-spec (cdr (assq 'company ph-bbdb-conversion-alist)) record nil)
649 bbdb-net (ph-parse-spec (cdr (assq 'net ph-bbdb-conversion-alist)) record nil)
650 bbdb-notes (ph-parse-spec (cdr (assq 'notes ph-bbdb-conversion-alist)) record nil))
651 (setq spec (cdr (assq 'address ph-bbdb-conversion-alist)))
652 (setq bbdb-address (delq nil (ph-parse-spec (if (listp (car spec))
653 spec
654 (list spec))
655 record t)))
656 (setq spec (cdr (assq 'phone ph-bbdb-conversion-alist)))
657 (setq bbdb-phones (delq nil (ph-parse-spec (if (listp (car spec))
658 spec
659 (list spec))
660 record t)))
661 ;; BBDB custom fields
662 (setq bbdb-notes (append (list (and bbdb-notes (cons 'notes bbdb-notes)))
663 (mapcar (function
664 (lambda (mapping)
665 (if (and (not (memq (car mapping)
666 '(name company net address phone notes)))
667 (setq value (ph-parse-spec (cdr mapping) record nil)))
668 (cons (car mapping) value))))
669 ph-bbdb-conversion-alist)))
670 (setq bbdb-notes (delq nil bbdb-notes))
671 (setq bbdb-record (bbdb-create-internal bbdb-name
672 bbdb-company
673 bbdb-net
674 bbdb-address
675 bbdb-phones
676 bbdb-notes))
677
678 (bbdb-display-records (list bbdb-record))
679 )))
680
681(defun ph-parse-spec (spec record recurse)
9fb87caf
RS
682 "Parse the conversion SPEC using RECORD.
683If RECURSE is non-nil then SPEC may be a list of atomic specs."
43a51f78
RS
684 (cond
685 ((or (stringp spec)
686 (symbolp spec)
687 (and (listp spec)
688 (symbolp (car spec))
689 (fboundp (car spec))))
690 (condition-case nil
691 (eval spec)
692 (void-variable nil)))
693 ((and recurse
694 (listp spec))
695 (mapcar '(lambda (spec-elem)
696 (ph-parse-spec spec-elem record nil))
697 spec))
698 (t
9fb87caf 699 (error "Invalid specification for `%s' in `ph-bbdb-conversion-alist'" spec))))
43a51f78
RS
700
701(defun ph-bbdbify-address (addr location)
9fb87caf 702 "Parse ADDR into a vector compatible with BBDB.
43a51f78 703ADDR should be an address string of no more than four lines or a
9fb87caf 704list of lines.
43a51f78 705The last line is searched for the zip code, city and state name.
e865033e 706LOCATION is used as the address location for bbdb."
43a51f78
RS
707 (let* ((addr-components (if (listp addr)
708 (reverse addr)
709 (reverse (split-string addr "\n"))))
710 (lastl (pop addr-components))
711 zip city state)
712 (setq addr-components (nreverse addr-components))
713 (cond
714 ;; American style
715 ((string-match "\\(\\w+\\)\\W*\\([A-Z][A-Z]\\)\\W*\\([0-9]+\\)" lastl)
716 (setq city (match-string 1 lastl)
717 state (match-string 2 lastl)
718 zip (string-to-number (match-string 3 lastl))))
719 ;; European style
720 ((string-match "\\([0-9]+\\)[ \t]+\\(.*\\)" lastl)
721 (setq city (match-string 2 lastl)
722 zip (string-to-number (match-string 1 lastl))))
723 (t
9fb87caf 724 (error "Cannot parse the address; see `ph-bbdb-conversion-alist'")))
43a51f78
RS
725 (vector location
726 (or (nth 0 addr-components) "")
727 (or (nth 1 addr-components) "")
728 (or (nth 2 addr-components) "")
729 (or city "")
730 (or state "")
731 zip)))
732
733(defun ph-bbdbify-phone (phone location)
9fb87caf 734 "Parse PHONE into a vector compatible with BBDB.
43a51f78
RS
735PHONE is either a string supposedly containing a phone number or
736a list of such strings which are concatenated.
9fb87caf 737LOCATION is used as the phone location for bbdb."
43a51f78
RS
738 (cond
739 ((stringp phone)
740 (let (phone-list)
741 (condition-case err
742 (setq phone-list (bbdb-parse-phone-number phone))
743 (error
e865033e 744 (if (string= "phone number unparsable." (ph-cadr err))
9fb87caf
RS
745 (if (not (y-or-n-p (format "BBDB claims %S to be unparsable--insert anyway? " phone)))
746 (error "Phone number unparsable")
43a51f78
RS
747 (setq phone-list (list (bbdb-string-trim phone))))
748 (signal (car err) (cdr err)))))
749 (if (= 3 (length phone-list))
750 (setq phone-list (append phone-list '(nil))))
751 (apply 'vector location phone-list)))
752 ((listp phone)
753 (vector location (mapconcat 'identity phone ", ")))
754 (t
9fb87caf 755 (error "Invalid phone specification"))))
43a51f78
RS
756
757;;}}}
758
759;;{{{ High-level interfaces (interactive functions)
760
761(defun ph-customize ()
762 "Customize the PH package."
763 (interactive)
764 (customize-group 'ph))
765
766(defun ph-set-server (server)
9fb87caf 767 "Set the PH server to SERVER."
43a51f78
RS
768 (interactive "sNew PH/QI Server: ")
769 (message "Selected PH/QI server is now %s" server)
770 (setq ph-server server))
771
772;;;###autoload
773(defun ph-get-email (name)
774 "Get the email field of NAME from the PH/QI directory server."
775 (interactive "sName: ")
776 (let ((email (cdaar (ph-query-internal name '(email)))))
777 (if (interactive-p)
778 (if email
779 (message "%s" email)
780 (message "No record matching %s" name)))
781 email))
782
783;;;###autoload
784(defun ph-get-phone (name)
785 "Get the phone field of NAME from the PH/QI directory server."
786 (interactive "sName: ")
787 (let ((phone (cdaar (ph-query-internal name '(phone)))))
788 (if (interactive-p)
789 (if phone
790 (message "%s" phone)
791 (message "No record matching %s" name)))
792 phone))
793
794(defun ph-get-field-list ()
795 "Return a list of valid field names for current server.
796When called interactively the list is formatted in a dedicated buffer
797otherwise a list of symbols is returned."
798 (interactive)
799 (ph-do-request "fields")
800 (if (interactive-p)
801 (let ((ph-duplicate-fields-handling-method 'list))
802 (ph-display-records (ph-parse-query-result) t))
803 (mapcar 'caar
804 (ph-parse-query-result)))
805 )
806
807;;;###autoload
808(defun ph-expand-inline (&optional replace)
9fb87caf 809 "Query the PH server, and expand the query string before point.
43a51f78 810The query string consists of the buffer substring from the point back to
9fb87caf
RS
811the preceding comma, colon or beginning of line. If it contains more than
812one word, the variable `ph-inline-query-format-list' controls to map these
43a51f78
RS
813onto CCSO database field names.
814After querying the server for the given string, the expansion specified by
9fb87caf
RS
815`ph-inline-expansion-format' is inserted in the buffer at point.
816If REPLACE is t, then this expansion replaces the name in the buffer.
817If `ph-expanding-overwrites-query' is t, that inverts the meaning of REPLACE."
43a51f78
RS
818 (interactive)
819 (let* ((end (point))
820 (beg (save-excursion
821 (if (re-search-backward "[:,][ \t]*"
822 (save-excursion
823 (beginning-of-line)
824 (point))
825 'move)
826 (goto-char (match-end 0)))
827 (point)))
828 (words (buffer-substring beg end))
829 query
830 query-alist
831 (query-format ph-inline-query-format-list)
832 response
833 response-strings
834 key val cell)
835
836 ;; Prepare the query
837 (if (or (not query-format)
838 (not (string-match "[ \t]+" words)))
839 (setq query words)
840 (setq words (split-string words "[ \t]+"))
841 (while (and words query-format)
842 (setq query-alist (cons (cons (car query-format) (car words)) query-alist))
843 (setq words (cdr words)
844 query-format (cdr query-format)))
845 (if words
846 (setcdr (car query-alist)
e865033e 847 (concat (ph-cdar query-alist) " "
43a51f78
RS
848 (mapconcat 'identity words " "))))
849 ;; Uniquify query-alist
850 (setq query-alist (nreverse query-alist))
851 (while query-alist
852 (setq key (caar query-alist)
e865033e 853 val (ph-cdar query-alist)
43a51f78
RS
854 cell (assq key query))
855 (if cell
856 (setcdr cell (concat val " " (cdr cell)))
857 (setq query (cons (car query-alist) query))))
858 (setq query-alist (cdr query-alist)))
859
860 (setq response (ph-query-internal query (cdr ph-inline-expansion-format)))
861
862 (if (null response)
863 (error "No match found")
864
865 ;; Process response through ph-inline-expansion-format
866 (while response
867 (setq response-strings
868 (cons (apply 'format
869 (car ph-inline-expansion-format)
870 (mapcar (function
871 (lambda (field)
872 (or (cdr (assq field (car response)))
873 "")))
874 (cdr ph-inline-expansion-format)))
875 response-strings))
876 (setq response (cdr response)))
877
878 (if (or
879 (and replace (not ph-expanding-overwrites-query))
880 (and (not replace) ph-expanding-overwrites-query))
881 (delete-region beg end))
882 (cond
883 ((or (= (length response-strings) 1)
884 (null ph-multiple-match-handling-method)
885 (eq ph-multiple-match-handling-method 'first))
886 (insert (car response-strings)))
887 ((eq ph-multiple-match-handling-method 'select)
888 (with-output-to-temp-buffer "*Completions*"
889 (display-completion-list response-strings)))
890 ((eq ph-multiple-match-handling-method 'all)
891 (insert (mapconcat 'identity response-strings ", ")))
892 ((eq ph-multiple-match-handling-method 'abort)
893 (error "There is more than one match for the query"))
894 ))
895 )
896 )
897
898;;;###autoload
899(defun ph-query-form (&optional get-fields-from-server)
9fb87caf 900 "Display a form to query the CCSO PH/QI nameserver.
43a51f78
RS
901If given a non-nil argument the function first queries the server
902for the existing fields and displays a corresponding form."
903 (interactive "P")
904 (let ((fields (or (and get-fields-from-server
905 (ph-get-field-list))
906 ph-form-fields))
907 (buffer (get-buffer-create "*PH/QI Query Form*"))
908 field-name
909 widget
910 (width 0)
911 inhibit-read-only
912 pt)
913 (switch-to-buffer buffer)
914 (setq inhibit-read-only t)
915 (erase-buffer)
916 (kill-all-local-variables)
917 (make-local-variable 'ph-form-widget-list)
918 (widget-insert "PH/QI Query Form\n")
919 (widget-insert "================\n\n")
920 (widget-insert "Current server is: " (or ph-server
921 (call-interactively 'ph-set-server)) "\n")
922 ;; Loop over prompt strings to find the biggest one
923 (setq fields
924 (mapcar (function
925 (lambda (field)
926 (setq field-name (or (and (assq field ph-fieldname-formstring-alist)
927 (cdr (assq field ph-fieldname-formstring-alist)))
928 (capitalize (symbol-name field))))
929 (if (> (length field-name) width)
930 (setq width (length field-name)))
931 (cons field field-name)))
932 fields))
933 ;; Insert the first widget out of the mapcar to leave the cursor
934 ;; in the first field
935 (widget-insert "\n\n" (format (concat "%" width "s: ") (cdr (car fields))))
936 (setq pt (point))
937 (setq widget (widget-create 'editable-field :size 15))
938 (setq ph-form-widget-list (cons (cons (car (car fields)) widget)
939 ph-form-widget-list))
940 (setq fields (cdr fields))
941 (mapcar (function
942 (lambda (field)
943 (widget-insert "\n\n" (format (concat "%" width "s: ") (cdr field)))
944 (setq widget (widget-create 'editable-field
945 :size 15))
946 (setq ph-form-widget-list (cons (cons (car field) widget)
947 ph-form-widget-list))))
948 fields)
949 (widget-insert "\n\n")
950 (widget-create 'push-button
951 :notify (lambda (&rest ignore)
952 (ph-process-form))
953 "Query Server")
954 (widget-insert " ")
955 (widget-create 'push-button
956 :notify (lambda (&rest ignore)
957 (ph-query-form))
958 "Reset Form")
959 (widget-insert " ")
960 (widget-create 'push-button
961 :notify (lambda (&rest ignore)
962 (kill-this-buffer))
963 "Quit")
964 (goto-char (1+ pt)) ; 1+ for some extent boundary reason
965 (use-local-map widget-keymap)
966 (widget-setup))
967 )
968
969(defun ph-bookmark-server (server)
9fb87caf
RS
970 "Add SERVER to the PH `servers' hotlist."
971 (interactive "sPH server: ")
43a51f78
RS
972 (if (member server ph-server-hotlist)
973 (error "%s is already in the hotlist" server)
974 (setq ph-server-hotlist (cons server ph-server-hotlist))
975 (ph-install-menu)
976 (ph-save-options)))
977
978(defun ph-bookmark-current-server ()
9fb87caf 979 "Add current server to the PH `servers' hotlist."
43a51f78
RS
980 (interactive)
981 (ph-bookmark-server ph-server))
982
983(defun ph-save-options ()
9fb87caf 984 "Save options (essentially the hotlist) to `ph-options-file'."
43a51f78
RS
985 (interactive)
986 (save-excursion
987 (set-buffer (find-file-noselect ph-options-file t))
988 ;; delete the previous setq
989 (let ((standard-output (current-buffer))
990 provide-p
991 setq-p)
992 (catch 'found
993 (while t
994 (let ((sexp (condition-case nil
995 (read (current-buffer))
996 (end-of-file (throw 'found nil)))))
997 (if (listp sexp)
998 (progn
999 (if (and (eq (car sexp) 'setq)
e865033e 1000 (eq (ph-cadr sexp) 'ph-server-hotlist))
43a51f78
RS
1001 (progn
1002 (delete-region (save-excursion
1003 (backward-sexp)
1004 (point))
1005 (point))
1006 (setq setq-p t)))
1007 (if (and (eq (car sexp) 'provide)
e865033e 1008 (equal (ph-cadr sexp) '(quote ph-options-file)))
43a51f78
RS
1009 (setq provide-p t))
1010 (if (and provide-p
1011 setq-p)
1012 (throw 'found t)))))))
1013 (if (eq (point-min) (point-max))
1014 (princ ";; This file was automatically generated by ph.el\n\n"))
1015 (if (not (bolp))
1016 (princ "\n"))
1017 (princ "(setq ph-server-hotlist '")
1018 (prin1 ph-server-hotlist)
1019 (princ ")\n")
1020 (if (not provide-p)
1021 (princ "(provide 'ph-options-file)\n"))
1022 (save-buffer)))
1023 )
1024
1025(defun ph-insert-record-at-point-into-bbdb ()
1026 "Insert record at point into the BBDB database.
1027This function can only be called from a PH/QI query result buffer."
1028 (interactive)
1029 (let ((record (and (overlays-at (point))
1030 (overlay-get (car (overlays-at (point))) 'ph-record))))
1031 (if (null record)
e865033e 1032 (error "Point is not over a record")
43a51f78
RS
1033 (ph-create-bbdb-record record))))
1034
1035(defun ph-try-bbdb-insert ()
9fb87caf 1036 "Call `ph-insert-record-at-point-into-bbdb' if on a record."
43a51f78
RS
1037 (interactive)
1038 (and (or (featurep 'bbdb)
1039 (prog1 (locate-library "bbdb") (message "")))
1040 (overlays-at (point))
1041 (overlay-get (car (overlays-at (point))) 'ph-record)
1042 (ph-insert-record-at-point-into-bbdb)))
1043
1044(defun ph-move-to-next-record ()
9fb87caf 1045 "Move to next record, in a buffer displaying PH query results."
43a51f78
RS
1046 (interactive)
1047 (if (not (eq major-mode 'ph-mode))
1048 (error "Not in a PH buffer")
1049 (let ((pt (next-overlay-change (point))))
1050 (if (< pt (point-max))
1051 (goto-char (1+ pt))
1052 (error "No more records after point")))))
1053
1054(defun ph-move-to-previous-record ()
9fb87caf 1055 "Move to previous record, in a buffer displaying PH query results."
43a51f78
RS
1056 (interactive)
1057 (if (not (eq major-mode 'ph-mode))
1058 (error "Not in a PH buffer")
1059 (let ((pt (previous-overlay-change (point))))
1060 (if (> pt (point-min))
1061 (goto-char pt)
1062 (error "No more records before point")))))
1063
1064
1065
1066;;}}}
1067
1068;;{{{ Menus an keymaps
1069
1070(require 'easymenu)
1071
1072(defvar ph-mode-map (let ((map (make-sparse-keymap)))
1073 (define-key map "q" 'kill-this-buffer)
1074 (define-key map "x" 'kill-this-buffer)
1075 (define-key map "f" 'ph-query-form)
1076 (define-key map "b" 'ph-try-bbdb-insert)
1077 (define-key map "n" 'ph-move-to-next-record)
1078 (define-key map "p" 'ph-move-to-previous-record)
1079 map))
1080(set-keymap-parent ph-mode-map widget-keymap)
1081
1082(defconst ph-tail-menu
1083 `(["---" nil nil]
1084 ["Query Form" ph-query-form t]
1085 ["Expand Inline" ph-expand-inline t]
43a51f78
RS
1086 ["---" nil nil]
1087 ["Get Email" ph-get-email t]
1088 ["Get Phone" ph-get-phone t]
1089 ["List Valid Field Names" ph-get-field-list t]
1090 ["---" nil nil]
1091 ,(cons "Customize" (cdr (custom-menu-create 'ph)))))
1092
1093(defconst ph-server-menu
1094 '(["---" ph-bookmark-server t]
1095 ["Bookmark Current Server" ph-bookmark-current-server t]
1096 ["New Server" ph-set-server t]))
1097
1098
1099(defun ph-menu ()
1100 (let (command)
1101 (append '("Ph")
1102 (list
1103 (append '("Server")
1104 (mapcar (function
1105 (lambda (server)
1106 (setq command (intern (concat "ph-set-server-" server)))
1107 (if (not (fboundp command))
1108 (fset command `(lambda ()
1109 (interactive)
1110 (setq ph-server ,server)
1111 (message "Selected PH/QI server is now %s" ,server))))
1112 (vector server command t)))
1113 ph-server-hotlist)
1114 ph-server-menu))
1115 ph-tail-menu)))
1116
1117(defun ph-install-menu ()
1118 (cond
1119 (ph-xemacs-p
1120 (add-submenu '("Tools") (ph-menu)))
1121 (ph-emacs-p
1122 (easy-menu-define ph-menu-map ph-mode-map "PH Menu" (ph-menu))
1123 (define-key
1124 global-map
1125 [menu-bar tools ph]
1126 (cons "Ph"
1127 (easy-menu-create-keymaps "Ph" (cdr (ph-menu))))))
1128 ))
1129
1130(ph-install-menu)
1131
1132
1133;;}}}
1134
1135(provide 'ph)
1136
1137;;; ph.el ends here