;;; Personal Database - Rolodex (BBDB, VCARD) ;;; Relevant URLs: ;;; http://bbdb.sourceforge.net/ ;;; http://www.jwz.org/bbdb/ ;;; http://www.cis.ohio-state.edu/htbin/rfc/rfc2426.html ;;; http://www.imc.org/pdi ;;; ;;; Copyright (C) 1999-2000 by Sam Steingold ;;; This is Free Software, covered by the GNU GPL (v2) ;;; See http://www.gnu.org/copyleft/gpl.html ;;; ;;; $Id: card.lisp,v 2.12 2002/01/26 19:37:21 sds Exp $ ;;; $Source: /cvsroot/clocc/clocc/src/cllib/card.lisp,v $ (eval-when (compile load eval) (require :cllib-base (translate-logical-pathname "clocc:src;cllib;base")) ;; `index-t' (require :cllib-withtype (translate-logical-pathname "cllib:withtype")) ;; `class-slot-list' (require :cllib-closio (translate-logical-pathname "cllib:closio")) ;; `pr-secs' (require :cllib-tilsla (translate-logical-pathname "cllib:tilsla")) ;; `substitute-subseq', `string-beg-with', `string-end-with', `split-string' (require :cllib-string (translate-logical-pathname "cllib:string")) ;; `date2time', `string->dttm', `dttm->string' (require :cllib-date (translate-logical-pathname "cllib:date")) ;; `url' (require :cllib-url (translate-logical-pathname "cllib:url"))) (in-package :cllib) (export '(*user-bbdb-file* *user-vcard-file* *user-native-file*)) ;;; ;;; {{{ definitions ;;; (eval-when (compile load eval) ; CMUCL (defclass name () ((first :type simple-string :initarg first :accessor name-first :documentation "the first name") (ini :type simple-string :initarg ini :accessor name-ini :documentation "the middle initial name") (last :type simple-string :initarg last :accessor name-last :documentation "the last name") (prefix :type simple-string :initarg prefix :accessor name-prefix :documentation "the prefix (like `PhD')") (suffix :type simple-string :initarg suffix :accessor name-suffix :documentation "the suffix (like `VII')") (aka :type cons :initarg aka :accessor name-aka :documentation "the list of aliases")) (:documentation "The name - with bells and whistles.")) (defclass phone () ((loc :type simple-string :initarg loc :accessor phone-loc :documentation "location") (nmb :type simple-string :initarg nmb :accessor phone-nmb :documentation "the phone number")) (:documentation "The phone - location and number.")) (defclass address () ((loc :type simple-string :initarg loc :accessor address-loc :documentation "location") (street1 :type simple-string :initarg street1 :accessor address-street1 :documentation "line one of the street address") (street2 :type simple-string :initarg street2 :accessor address-street2 :documentation "line two of the street address") (street3 :type simple-string :initarg street3 :accessor address-street3 :documentation "line three of the street address") (city :type simple-string :initarg city :accessor address-city :documentation "the city") (state :type simple-string :initarg state :accessor address-state :documentation "the state") (zip :type simple-string :initarg zip :accessor address-zip :documentation "the zip code") (country :initform "USA" :type simple-string :initarg country :accessor address-country :documentation "the country")) (:documentation "The full mailing address.")) (defclass card () ((label :type simple-string :initarg label :accessor card-label :documentation "The unique record ID.") (name :type name :initarg name :accessor card-name :documentation "The name of the person.") (addrl :type cons :initarg addrl :accessor card-addrl :documentation "The list of addresses.") (phonel :type cons :initarg phonel :accessor card-phonel :documentation "The list of phones.") (emaill :type cons :initarg emaill :accessor card-emaill :documentation "The list of e-mail addresses.") (urll :type list :initarg urll :accessor card-urll :documentation "The list of URLs.") (security :type simple-string :initarg security :accessor card-security :documentation "PGP or X509 key.") (note :type simple-string :initarg note :accessor card-note :documentation "Random note not fitting anything else.") (org :type simple-string :initarg org :accessor card-org :documentation "The organization or company.") (title :type simple-string :initarg title :accessor card-title :documentation "The title of the person.") (tz :type (rational -24 24) :initarg tz :accessor card-tz :documentation "The time zone.") (geo :type complex :initarg geo :accessor card-geo :documentation "The geografic coordinates, in degrees.") (class :type symbol :initarg class :accessor card-class :documentation "Access classification: PRIVATE, PUBLIC &c") ;; (bday (created :type (integer 0) :initarg created :accessor card-created :documentation "The creation time, in seconds since the epoch.") (timestamp :type (integer 0) :initarg timestamp :accessor card-timestamp :documentation "The last modification time.")) (:documentation "The personal information record.")) ) (defun slot-val (obj slot &optional default) (or (when (slot-boundp obj slot) (slot-value obj slot)) default)) ;;; ;;; }}}{{{ search ;;; (defcustom *user-cards* list nil "*The user database of records.") (defun object-match-p (cc nm) "Check whether the object matches the string." (declare (type card cc) (simple-string nm)) (dolist (slot (class-slot-list cc)) (when (slot-boundp cc slot) (let ((val (slot-value cc slot))) (typecase val (standard-object (when (object-match-p val nm) (return t))) (string (when (search nm val) (return t)))))))) (defun find-card (nm &optional (cards *user-cards*)) "Return the list of cards matching NM." (declare (simple-string nm)) (loop :for cc :of-type card :in cards :when (object-match-p cc nm) :collect cc)) ;;; ;;; }}}{{{ output ;;; (eval-when (compile load eval) ; CMUCL (defstruct card-output (card nil :type symbol) (name nil :type symbol) (phone nil :type symbol) (address nil :type symbol)) ) (defcustom *card-output-type* (or null card-output) nil "The type of output for CARD. Valid value are instances of `card-output' or NIL (for #[card ...]). See constants `+card-output-bbdb+', `+card-output-vcard+', `+card-output-pretty+'.") (defcustom *user-bbdb-file* pathname (mk-path (user-homedir-pathname) :name ".bbdb") "*The path to the user's BBDB file.") (defcustom *user-vcard-file* pathname (mk-path (user-homedir-pathname) :directory '(:relative ".gnome") :name "GnomeCard.gcrd") "*The path to the user's VCARD file.") (defcustom *user-native-file* pathname (mk-path (user-homedir-pathname) :name ".rolodex") "*The path to the user's native data file.") (defconst +card-output-bbdb+ card-output (make-card-output :card 'card-print-as-bbdb :name 'name-print-as-bbdb :phone 'phone-print-as-bbdb :address 'address-print-as-bbdb) "The constant, a valid value for `*card-output-type*'.") (defconst +card-output-vcard+ card-output (make-card-output :card 'card-print-as-vcard :name 'name-print-as-vcard :phone 'phone-print-as-vcard :address 'address-print-as-vcard) "The constant, a valid value for `*card-output-type*'.") (defconst +card-output-pretty+ card-output (make-card-output :card 'card-print-as-pretty :name 'name-print-as-pretty :phone 'phone-print-as-pretty :address 'address-print-as-pretty) "The constant, a valid value for `*card-output-type*'.") (defun card-org/title (cc) "Merge the TITLE with ORG." (when (or (slot-boundp cc 'org) (slot-boundp cc 'title)) (format nil "~@[~a~]~:[~;; ~]~@[~a~]" (slot-val cc 'org) (and (slot-boundp cc 'org) (slot-boundp cc 'title)) (slot-val cc 'title)))) (defun card-print-as-bbdb (cc out) (declare (type card cc) (stream out)) (let ((*print-right-margin* nil) (*print-pretty* nil)) (if (slot-boundp cc 'name) (format out "[~a " (card-name cc)) (write-string "[nil nil nil " out)) (if (or (slot-boundp cc 'org) (slot-boundp cc 'title)) (format out "\"~a~:[~;; ~a~]\" " (slot-val cc 'org) (and (slot-boundp cc 'org) (slot-boundp cc 'title)) (slot-val cc 'title)) (write-string "nil " out)) (format out "~s ~s ~s ~s (" (card-org/title cc) (slot-val cc 'phonel) (slot-val cc 'addrl) (slot-val cc 'emaill)) (when (slot-boundp cc 'note) (format out "(notes . ~s) " (card-note cc))) (format out "~@[(url . \"~{~a~^, ~}\") ~]~ \(creation-date . \"~a\") (timestamp . \"~a\")) nil]" (slot-val cc 'urll) (dttm->string (card-created cc) :format :date) (dttm->string (card-timestamp cc) :format :date)))) (defconstant +card-vcard-begin+ "BEGIN:VCARD") (defconstant +card-vcard-end+ "END:VCARD") (defun card-print-as-vcard (cc out) (declare (type card cc) (stream out)) (format out "~&~a~%FN:~a~%~@[N:~a~%~]~{ADR:~a~%~}~{TEL:~a~%~}~ ~{EMAIL:~a~%~}~{URL:~a~%~}~@[TITLE:~a~%~]~@[ORG:~a~%~]~@[NOTE:~a~%~]" +card-vcard-begin+ (card-label cc) (slot-val cc 'name) (slot-val cc 'addrl) (slot-val cc 'phonel) (slot-val cc 'emaill) (slot-val cc 'urll) (slot-val cc 'title) (slot-val cc 'org) (slot-val cc 'note)) (when (slot-boundp cc 'tz) (format out "TZ:~@/pr-secs/~%" (* 3600 (card-tz cc)))) (when (slot-boundp cc 'geo) (format out "GEO:~f:~f~%" (realpart (card-geo cc)) (imagpart (card-geo cc)))) (format out "REV:~a~%CRE:~a~%~@[CLASS:~a~%~]~a~%" (dttm->string (card-timestamp cc) :format :datetime) (dttm->string (card-created cc) :format :datetime) (slot-val cc 'security) +card-vcard-end+)) (defun card-print-as-pretty (cc out) (declare (type card cc) (stream out)) (format out "~@[~a~%~]~@[[~a]~%~]" (slot-val cc 'name) (card-org/title cc)) (when (slot-boundp cc 'emaill) (format out "[email:~10t~{~<~%~9t ~1,74:; <~a>~>~^,~}.]~%" (card-emaill cc))) (when (slot-boundp cc 'urll) (format out "[URL:~10t~{~<~%~9t ~1,74:; <~a>~>~^,~}.]~%" (card-urll cc))) (format out "~{~a~%~}~{~a~%~}~@[[~a]~%~]{~a ~a}~2%" (slot-val cc 'phonel) (slot-val cc 'addrl) (slot-val cc 'note) (dttm->string (card-created cc) :format :datetime) (dttm->string (card-timestamp cc) :format :datetime))) (defun name-print-as-bbdb (nm out) (declare (type name nm) (stream out)) (format out "~:[~*~;~:*\"~a~@[ ~a~]\"~] ~s ~s" (slot-val nm 'first) (slot-val nm 'ini) (slot-val nm 'last) (slot-val nm 'aka))) (defun name-print-as-vcard (nm out) (declare (type name nm) (stream out)) (format out "~@[~a~];~@[~a~];~@[~a~];~@[~a~];~@[~a~]" (slot-val nm 'last) (slot-val nm 'first) (slot-val nm 'ini) (slot-val nm 'prefix) (slot-val nm 'suffix))) (defun name-print-as-pretty (nm out) (declare (type name nm) (stream out)) (format out "~@[~a ~]~@[~a ~]~@[~a ~]~@[~a~]~@[, ~a~]" (slot-val nm 'prefix) (slot-val nm 'first) (slot-val nm 'ini) (slot-val nm 'last) (slot-val nm 'suffix))) (defun phone-print-as-bbdb (ph out) (declare (type phone ph) (stream out)) (format out "[~s ~s]" (phone-loc ph) (phone-nmb ph))) (defun phone-print-as-vcard (ph out) (declare (type phone ph) (stream out)) (format out "~a:~a" (phone-loc ph) (phone-nmb ph))) (defun phone-print-as-pretty (ph out) (declare (type phone ph) (stream out)) (format out " ~20@a: ~a" (phone-loc ph) (phone-nmb ph))) (defun address-print-as-bbdb (adrs out) (declare (type address adrs) (stream out)) (format out "[~s ~s ~s ~s ~s ~s ~s ~s]" (slot-val adrs 'loc "") (slot-val adrs 'street1 "") (slot-val adrs 'street2 "") (slot-val adrs 'street3 "") (slot-val adrs 'city "") (slot-val adrs 'state "") (slot-val adrs 'zip "") (slot-val adrs 'country ""))) (defun address-print-as-vcard (adrs out) (declare (type address adrs) (stream out)) (format out "~a:~a;~a;~a;~a;~a;~a;~a" (slot-val adrs 'loc "") (slot-val adrs 'street1 "") (slot-val adrs 'street2 "") (slot-val adrs 'street3 "") (slot-val adrs 'city "") (slot-val adrs 'state "") (slot-val adrs 'zip "") (slot-val adrs 'country ""))) (defun address-print-as-pretty (adrs out) (declare (type address adrs) (stream out)) (format out "~a:~%~@[ ~a~%~]~@[ ~a~%~]~@[ ~a~%~]~@[ ~a ~]~@[~a ~]~@[~a ~]~@[~a~]" (slot-val adrs 'loc) (slot-val adrs 'street1) (slot-val adrs 'street2) (slot-val adrs 'street3) (slot-val adrs 'city) (slot-val adrs 'state) (slot-val adrs 'zip) (slot-val adrs 'country))) (defmacro define-print-method (cs) `(defmethod print-object ((cc ,cs) (out stream)) (cond ((or *print-readably* (null *card-output-type*)) (call-next-method)) ((card-output-p *card-output-type*) (funcall (,(symbol-concat 'card-output- cs) *card-output-type*) cc out)) (t (error 'code :proc 'print-object :args (list *card-output-type*) :mesg "Illegal value of `*card-output-type*': `~s'"))))) (define-print-method card) (define-print-method name) (define-print-method phone) (define-print-method address) (defun init-sans-null-args (obj list) "(a 10 b nil c 1 d nil) --> (a 10 c 1)" (loop :for arg :in list :by #'cddr :and val :in (cdr list) :by #'cddr :when val :do (setf (slot-value obj arg) val))) ;; has to be here since it uses `+card-output-pretty+' (defmethod initialize-instance ((cc card) &rest args) (init-sans-null-args cc args) ;; init `label' from `name' and `emaill' (when (and (not (slot-boundp cc 'label)) (or (slot-boundp cc 'name) (slot-boundp cc 'emaill))) (let ((*card-output-type* +card-output-pretty+)) (setf (card-label cc) (format nil "~@[~a ~]~@[<~a>~]" (slot-val cc 'name) (car (slot-val cc 'emaill)))))) ;; init `timestamp' and `created' from `get-universal-time' (unless (and (slot-boundp cc 'timestamp) (slot-boundp cc 'created)) (let ((tm (get-universal-time))) (unless (slot-boundp cc 'timestamp) (setf (card-timestamp cc) tm)) (unless (slot-boundp cc 'created) (setf (card-created cc) tm)))) ;; init `tz' from `geo' (when (and (slot-boundp cc 'geo) (not (slot-boundp cc 'tz))) (setf (card-tz cc) (round (realpart (card-geo cc)) 15))) ;; replace #\Newline with \n in multiline strings. (when (slot-boundp cc 'note) (setf (card-note cc) (substitute-subseq (card-note cc) (string #\Newline) "\\n"))) (when (slot-boundp cc 'security) (setf (card-note cc) (substitute-subseq (card-security cc) (string #\Newline) "\\n"))) cc) (defcustom *card-apellations* list '("Mr." "Mrs." "Ms." "Miss" "Dr" "Dr." "Prof.") "*The list of recognizable apellations.") (defcustom *card-suffixes* list '("PhD" "Ph.D." "PhD." "MD" "CPA" "Jr" "3rd") "*The list of recognizable apellations.") (defmethod initialize-instance ((nn name) &rest args) (init-sans-null-args nn args) (when (slot-boundp nn 'first) (let* ((fi (name-first nn)) (len (length fi))) (declare (simple-string fi)) ;; init `prefix' from `first' (unless (slot-boundp nn 'prefix) (dolist (st *card-apellations*) (declare (simple-string st)) (when (string-beg-with st fi len) (setf (name-prefix nn) st fi (string-left-trim +whitespace+ (subseq fi (length st))) (name-first nn) fi) (return)))) ;; init `ini' from `first' (unless (slot-boundp nn 'ini) (let ((pos (position #\Space fi :from-end t))) (when pos (setf (name-ini nn) (subseq fi (1+ pos)) (name-first nn) (subseq fi 0 pos))))))) ;; init `suffix' from `last' (when (and (slot-boundp nn 'last) (not (slot-boundp nn 'suffix))) (let* ((la (name-last nn)) (len (length la))) (declare (simple-string la)) (dolist (st *card-suffixes*) (declare (simple-string st)) (when (string-end-with st la len) (setf (name-suffix nn) st (name-last nn) (string-right-trim +whitespace+ (subseq la 0 (- len (length st)))) len (length (name-last nn))) ;; remove the taling comma (when (char= #\, (schar (name-last nn) (1- len))) (setf (name-last nn) (subseq (name-last nn) 0 (1- len)))) (return))))) nn) ;;; ;;; }}}{{{ input ;;; (declaim (ftype (function (simple-vector) (values phone)) vector-to-phone)) (defun vector-to-phone (vec) "Convert the vector (from BBDB) to a PHONE instance." (declare (simple-vector vec)) (make-instance 'phone 'loc (aref vec 0) 'nmb (typecase (aref vec 1) (string (aref vec 1)) (number (format nil "(~3,'0d) ~3,'0d-~4,'0d~:[ x~d~;~]" (aref vec 1) (aref vec 2) (aref vec 3) (zerop (aref vec 4)) (aref vec 4)))))) (declaim (ftype (function (simple-vector) (values address)) vector-to-address)) (defun vector-to-address (vec) "Convert the vector (from BBDB) to an ADDRESS instance." (declare (simple-vector vec)) (let ((ad (make-instance 'address 'loc (aref vec 0)))) (macrolet ((putslot (nn slot) `(unless (zerop (length (the simple-string (aref vec ,nn)))) (setf (slot-value ad ',slot) (aref vec ,nn))))) (putslot 1 street1) (putslot 2 street2) (putslot 3 street3) (putslot 4 city) (putslot 5 state)) (setf (address-zip ad) (let ((zip (aref vec 6))) (typecase zip (number (format nil "~5,'0d" zip)) (string zip) (list (format nil "~5,'0d-~4,'0d" (first zip) (second zip)))))) ad)) (declaim (ftype (function (simple-vector) (values card)) vector-to-card)) (defun vector-to-card (vec) "Convert the vector (as in BBDB) to a CARD." (declare (simple-vector vec)) (let ((nts (aref vec 7)) tmp) (make-instance 'card 'name (make-instance 'name 'first (aref vec 0) 'last (aref vec 1) 'aka (aref vec 2)) 'org (aref vec 3) 'phonel (mapcar #'vector-to-phone (aref vec 4)) 'addrl (mapcar #'vector-to-address (aref vec 5)) 'emaill (aref vec 6) 'note (cdr (assoc 'notes nts :test #'eq)) 'created (when (setq tmp (assoc 'creation-date nts :test #'eq)) (date2time (date (cdr tmp)))) 'timestamp (when (setq tmp (assoc 'timestamp nts :test #'eq)) (date2time (date (cdr tmp)))) 'urll (mapcan (lambda (ff) (mapcar #'url (split-string (cdr (assoc ff nts :test #'eq)) #(#\Space #\Newline #\,)))) '(www ftp gopher telnet))))) (defun card-read-bbdb (in ra) "Read CARD from a BBDB stream. Suitable for `read-list-from-stream'." (declare (stream in) (simple-vector ra)) (handler-case (values (vector-to-card ra) (read in nil +eof+)) (error (co) (format t "~& *** [vector-to-card] Broken on~%~s~%" ra) (error co)))) (defun card-read-vcard (in ra) "Read CARD from a vCard stream. Suitable for `read-list-from-stream'." (declare (stream in)) (unless (string= ra +card-vcard-begin+) (warn "~s: a VCARD should start with ~s, not with ~s" 'card-read-vcard +card-vcard-begin+ ra)) (macrolet ((pushslot (val obj slot) `(if (slot-boundp ,obj ',slot) (push ,val (slot-value ,obj ',slot)) (setf (slot-value ,obj ',slot) (list ,val))))) (loop :with cc :of-type card = (make-instance 'card) :for str :of-type (or null simple-string) = (read-line in nil nil) :for len :of-type index-t = (length str) :while (and str (not (string= str +card-vcard-end+))) :do (cond ((string-beg-with "FN:" str len) (setf (card-label cc) (subseq str 3))) ((string-beg-with "N:" str len) (let ((ll (split-string (subseq str 2) ";" :strict t))) (setf (card-name cc) (make-instance 'name 'last (first ll) 'first (second ll) 'ini (third ll) 'prefix (fourth ll) 'suffix (fifth ll))))) ((string-beg-with "ADR;" str len) (let ((ll (split-string (subseq str 4) ":;" :strict t))) (pushslot (make-instance 'address 'loc (first ll) 'street1 (second ll) 'street2 (third ll) 'street3 (fourth ll) 'city (fifth ll) 'state (sixth ll) 'zip (seventh ll) 'country (eighth ll)) cc addrl))) ((string-beg-with "TEL;" str len) (let ((pos (position #\: str :start 4))) (declare (type index-t pos)) (pushslot (make-instance 'phone 'loc (subseq str 4 pos) 'nmb (subseq str (1+ pos))) cc phonel))) ((string-beg-with "EMAIL;" str len) (pushslot (subseq str (1+ (position #\: str :start 6))) cc emaill)) ((string-beg-with "TZ:" str len) (let ((ll (split-string (subseq str 3) ":"))) (setf (card-tz cc) (+ (parse-integer (first ll)) (/ (parse-integer (second ll)) 60) (if (cddr ll) (/ (parse-integer (third ll)) 3600) 0))))) ((string-beg-with "URL:" str len) (pushslot (url (subseq str 4)) cc urll)) ((string-beg-with "ORG:" str len) (setf (card-org cc) (subseq str 4))) ((string-beg-with "TITLE:" str len) (setf (card-title cc) (subseq str 6))) ((string-beg-with "NOTE:" str len) (setf (card-note cc) (subseq str 5))) ((string-beg-with "REV:" str len) (setf (card-timestamp cc) (string->dttm (subseq str 4)))) ((string-beg-with "CRE:" str len) (setf (card-created cc) (string->dttm (subseq str 4)))) (t (warn "~s: unknown field: ~s~%" 'card-read-vcard str))) :finally (dolist (slot '(addrl urll emaill phonel)) (when (slot-boundp cc slot) (setf (slot-value cc slot) (nreverse (slot-value cc slot))))) :finally (loop :for str = (read-line in nil nil) :until (or (null str) (string= str +card-vcard-begin+)) :finally (return-from card-read-vcard (values (initialize-instance cc) (or str +eof+))))))) #+nil (progn ;;; BBDB i/o (setq *user-cards* (read-list-from-file *user-bbdb-file* #'card-read-bbdb :readtable +elisp-readtable+)) (let ((*card-output-type* +card-output-bbdb+)) (write-list-to-file *user-cards* *user-bbdb-file*)) ;;; vCard i/o (let ((*card-output-type* +card-output-vcard+)) (print *user-cards*) (write-list-to-file *user-cards* *user-vcard-file*)) (setq *user-cards* (read-list-from-file *user-vcard-file* #'card-read-vcard :read-ahead-function #'read-line)) (setq *user-cards* (read-list-from-file (mk-path (user-homedir-pathname) :name ".vcard") #'card-read-vcard :read-ahead-function #'read-line)) ;;; native i/o (let ((*card-output-type* nil)) (write-to-file *user-cards* *user-native-file*)) (setq *user-cards* (read-from-file *user-native-file* :readtable +clos-readtable+)) ;;; Pretty printing (let ((*card-output-type* +card-output-pretty+)) (write-list-to-stream *user-cards* *standard-output*)) ;; testing (let ((*card-output-type* +card-output-vcard+)) (print (car *user-cards*))) ) ;;; }}} (provide :cllib-card) ;;; file card.lisp ends here