1 ;;; bbdb-vcard.el --- vCard import/export for BBDB
3 ;; Copyright (c) 2010 Bert Burgemeister
5 ;; Author: Bert Burgemeister <trebbu@googlemail.com>
6 ;; Keywords: data calendar mail news
7 ;; URL: http://github.com/trebb/bbdb-vcard
10 ;; This program is free software; you can redistribute it and/or
11 ;; modify it under the terms of the GNU General Public License as
12 ;; published by the Free Software Foundation; either version 2, or (at
13 ;; your option) any later version.
15 ;; This program is distributed in the hope that it will be useful, but
16 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 ;; General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING. If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
25 ;; The exporter functionality is based on code from
26 ;; bbdb-vcard-export.el by Jim Hourihan and Alex Schroeder.
27 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
35 ;; Import and export of vCards as defined in RFC 2425 and RFC 2426
36 ;; to/from The Insidious Big Brother Database (BBDB).
45 ;; On a file, a buffer or a region containing one or more vCards, use
46 ;; `bbdb-vcard-import-file', `bbdb-vcard-import-buffer', or
47 ;; `bbdb-vcard-import-region' respectively to import them into BBDB.
49 ;; Preferred input format is vCard version 3.0. Version 2.1 vCards
50 ;; are converted to version 3.0 on import.
56 ;; In buffer *BBDB*, press v to export the record under point. Press
57 ;; * v to export all records in buffer into one vCard file. Press *
58 ;; C-u v to export them into one file each.
60 ;; To put one or all vCard(s) into the kill ring, press V or * V
63 ;; Exported vCards are always version 3.0. They can be re-imported
64 ;; without data loss with one exception: North American phone numbers
65 ;; lose their structure and are stored as flat strings.
68 ;; There are a few customization variables grouped under `bbdb-vcard'.
74 ;; Put this file and file vcard.el into your `load-path' and add the
75 ;; following line to your Emacs initialization file:
77 ;; (require 'bbdb-vcard)
86 ;; For conversion of v2.1 vCards into v3.0 on import, Noah Friedman's
87 ;; vcard.el is needed.
89 ;; An existing BBDB record is extended by new information from a vCard
91 ;; (a) if name and company and an email address match
92 ;; (b) or if name and company match
93 ;; (c) or if name and an email address match
94 ;; (d) or if name and birthday match
95 ;; (e) or if name and a phone number match.
97 ;; Otherwise, a fresh BBDB record is created.
99 ;; When `bbdb-vcard-try-merge' is set to nil, there is always a fresh
102 ;; In cases (c), (d), and (e), if the vCard has ORG defined, this ORG
103 ;; would overwrite an existing Company in BBDB.
105 ;; Phone numbers are always imported as strings.
107 ;; For vCard types that have more or less direct counterparts in BBDB,
108 ;; labels and parameters are translated and structured values
109 ;; (lastname; firstname; additional names; prefixes etc.) are
110 ;; converted appropriately with the risk of some (hopefully
111 ;; unessential) information loss. For labels of the vCard types ADR
112 ;; and TEL, parameter translation is defined in
113 ;; `bbdb-vcard-import-translation-table'.
115 ;; If there is a REV element, it is stored in BBDB's creation-date in
116 ;; newly created BBDB records, or discarded for existing ones.
118 ;; VCard type prefixes (A.ADR:..., B.ADR:... etc.) are stripped off
119 ;; and discarded from the following types: N, FN, NICKNAME, ORG (first
120 ;; occurrence), ADR, TEL, EMAIL, URL, BDAY (first occurrence), NOTE.
122 ;; VCard types that are prefixed `X-BBDB-' are stored in BBDB without
125 ;; VCard type X-BBDB-ANNIVERSARY may contain (previously exported)
126 ;; newline-separated non-birthday anniversaries that are meant to be
129 ;; All remaining vCard types that don't match the regexp in
130 ;; `bbdb-vcard-skip-on-import' and that have a non-empty value are
131 ;; stored unaltered in the BBDB Notes alist where, for instance,
132 ;; `TZ;VALUE=text:-05:00' is stored as `(tz\;value=text . "-05:00")'.
133 ;; From the BBDB data fields AKA, Phones, Addresses, Net Addresses,
134 ;; and Notes, duplicates are removed, respectively.
136 ;; VCards found inside other vCards (as values of type AGENT) are
140 ;; Handling of the individual types defined in RFC2426 during import
141 ;; (assuming default label translation and no vCard type exclusion):
143 ;; |----------------------+----------------------------------------|
144 ;; | VCARD TYPE; | STORAGE IN BBDB |
146 ;; |----------------------+----------------------------------------|
148 ;; |----------------------+----------------------------------------|
149 ;; | N | First occurrence: |
154 ;; | | AKAs (append) |
155 ;; |----------------------+----------------------------------------|
156 ;; | FN | AKAs (append) |
157 ;; | NICKNAME | AKAs (append) |
158 ;; |----------------------+----------------------------------------|
159 ;; | ORG | First occurrence: |
164 ;; | | (repeatedly) |
165 ;; |----------------------+----------------------------------------|
166 ;; | ADR;TYPE=x,HOME,y | Addresses<Home |
167 ;; | ADR;TYPE=x;TYPE=HOME | Addresses<Home |
168 ;; | ADR;TYPE=x,WORK,y | Addresses<Office |
169 ;; | ADR;TYPE=x;TYPE=WORK | Addresses<Office |
170 ;; | ADR;TYPE=x,y,z | Addresses<x,y,z |
171 ;; | ADR;TYPE=x;TYPE=y | Addresses<x,y |
172 ;; | ADR | Addresses<Office |
173 ;; |----------------------+----------------------------------------|
174 ;; | TEL;TYPE=x,HOME,y | Phones<Home (append) |
175 ;; | TEL;TYPE=x;TYPE=HOME | Phones<Home (append) |
176 ;; | TEL;TYPE=x,WORK,y | Phones<Office (append) |
177 ;; | TEL;TYPE=x;TYPE=WORK | Phones<Office (append) |
178 ;; | TEL;TYPE=x,CELL,y | Phones<Mobile (append) |
179 ;; | TEL;TYPE=x;TYPE=CELL | Phones<Mobile (append) |
180 ;; | TEL;TYPE=x,y,z | Phones<x,y,z (append) |
181 ;; | TEL;TYPE=x;TYPE=y | Phones<x,y (append) |
182 ;; | TEL | Phones<Office (append) |
183 ;; |----------------------+----------------------------------------|
184 ;; | EMAIL;TYPE=x,y,z | Net-Addresses (append) |
185 ;; | URL | Notes<www |
186 ;; |----------------------+----------------------------------------|
187 ;; | BDAY | Notes<anniversary (append as birthday) |
188 ;; | X-BBDB-ANNIVERSARY | Notes<anniversary (append) |
189 ;; |----------------------+----------------------------------------|
190 ;; | NOTE | Notes<notes (append) |
191 ;; | REV | Notes<creation-date |
192 ;; | CATEGORIES | Notes<mail-alias (append) |
193 ;; | SORT-STRING | Notes<sort-string |
194 ;; | KEY | Notes<key |
195 ;; | GEO | Notes<geo |
197 ;; | PHOTO | Notes<photo |
198 ;; | LABEL | Notes<label |
199 ;; | LOGO | Notes<logo |
200 ;; | SOUND | Notes<sound |
201 ;; | TITLE | Notes<title |
202 ;; | ROLE | Notes<role |
203 ;; | AGENT | Notes<agent |
204 ;; | MAILER | Notes<mailer |
205 ;; | UID | Notes<uid |
206 ;; | PRODID | Notes<prodid |
207 ;; | CLASS | Notes<class |
208 ;; | X-foo | Notes<x-foo |
209 ;; | X-BBDB-bar | Notes<bar |
210 ;; |----------------------+----------------------------------------|
211 ;; | anyJunK;a=x;b=y | Notes<anyjunk;a=x;b=y |
212 ;; |----------------------+----------------------------------------|
218 ;; VCard types N (only fields lastname, firstname) and FN both come
221 ;; Members of BBDB field AKA are stored comma-separated under the
222 ;; vCard type NICKNAME.
224 ;; Labels of Addresses and Phones are translated as defined in
225 ;; `bbdb-vcard-export-translation-table' into type parameters of
226 ;; vCard types ADR and TEL, respectively.
228 ;; In vCard type ADR, fields postbox and extended address are always
229 ;; empty. Newlines which subdivide BBDB Address fields are converted
230 ;; into commas subdividing vCard ADR fields.
232 ;; The value of 'anniversary in Notes is supposed to be subdivided by
233 ;; newlines. The birthday part (either just a date or a date followed
234 ;; by \"birthday\") is stored under vCard type BDAY. The rest is
235 ;; stored newline-separated in the non-standard vCard type
236 ;; X-BBDB-ANNIVERSARY.
238 ;; Field names listed in `bbdb-vcard-x-bbdb-candidates' are in the
239 ;; exported vCard prepended by `X-BBDB-'.
241 ;; The creation-date of the BBDB record is stored as vCard type REV.
243 ;; Remaining members of BBDB Notes are exported to the vCard without
259 (defconst bbdb-vcard-version
"0.2"
260 "Version of the vCard importer/exporter.
261 The major part increases on user-visible changes.")
267 (defgroup bbdb-vcard nil
268 "Customizations for vCards"
271 (defcustom bbdb-vcard-skip-on-import
"X-GSM-"
272 "Regexp describing vCard elements that are to be discarded during import.
273 Example: `X-GSM-\\|X-MS-'."
277 (defcustom bbdb-vcard-skip-valueless t
278 "Skip vCard element types with an empty value.
279 Nil means insert empty types into BBDB."
283 (defcustom bbdb-vcard-import-translation-table
284 '(("CELL\\|CAR" .
"Mobile")
286 ("HOME" .
"Home") ; translates e.g. "dom,home,postal,parcel" to "Home"
287 ("^$" .
"Office")) ; acts as a default for parameterless ADR or TEL
288 "Label translation on vCard import.
289 Alist with translations of location labels for addresses and phone
290 numbers. Cells are (VCARD-LABEL-REGEXP . BBDB-LABEL). One entry
291 should map a default BBDB label to the empty string (`\"^$\"') which
292 corresponds to unlabelled vCard elements."
294 :type
'(alist :key-type
295 (choice regexp
(const :tag
"Empty (as default)" "^$"))
298 (defcustom bbdb-vcard-try-merge t
299 "Try to merge vCards into existing BBDB records.
300 Nil means create a fresh bbdb record each time a vCard is read."
304 (defcustom bbdb-vcard-type-canonicalizer
'upcase
305 "Function to apply to vCard type names on export.
306 Most reasonable choices are `upcase' and `downcase'."
310 (defcustom bbdb-vcard-x-bbdb-candidates
318 aka
) ; not sure what this is for
319 "List of translatable BBDB user field names.
320 On export to a vCard, they are transformed into vCard-compliant
321 extended types by prepending `X-BBDB-'. On (re-)import, this prefix
324 :type
'(repeat symbol
))
326 (defcustom bbdb-vcard-export-translation-table
327 '(("Mobile" .
"CELL")
329 "Label translation on vCard export.
330 Alist with translations of location labels for addresses and phone
331 numbers. Cells are (BBDB-LABEL-REGEXP . VCARD-LABEL)."
333 :type
'(alist :key-type
334 (choice regexp
(const :tag
"Empty (as default)" "^$"))
337 (defcustom bbdb-vcard-export-coding-system
338 'utf-8-dos
; dos line endings mandatory according to RFC 2426
339 "Coding system to use when writing vCard files."
343 (defcustom bbdb-vcard-default-dir
"~/exported-vcards/"
344 "Default storage directory for exported vCards.
345 Nil means current directory."
347 :type
'(choice directory
(const :tag
"Current directory" nil
)))
354 (defun bbdb-vcard-import-region (begin end
)
355 "Import the vCards between BEGIN and END into BBDB.
356 Existing BBDB records may be altered."
358 (bbdb-vcard-iterate-vcards 'bbdb-vcard-import-vcard
359 (buffer-substring-no-properties begin end
)))
362 (defun bbdb-vcard-import-buffer (vcard-buffer)
363 "Import vCards from VCARD-BUFFER into BBDB.
364 Existing BBDB records may be altered."
365 (interactive (list (current-buffer)))
366 (set-buffer vcard-buffer
)
367 (bbdb-vcard-import-region (point-min) (point-max)))
370 (defun bbdb-vcard-import-file (vcard-file)
371 "Import vCards from VCARD-FILE into BBDB.
372 If VCARD-FILE is a wildcard, import each matching file. Existing BBDB
373 records may be altered."
374 (interactive "FvCard file (or wildcard): ")
375 (dolist (vcard-file (file-expand-wildcards vcard-file
))
377 (insert-file-contents vcard-file
)
378 (bbdb-vcard-import-region (point-min) (point-max)))))
381 (defun bbdb-vcard-export
382 (filename-or-directory all-records-p one-file-per-record-p
)
383 "From Buffer *BBDB*, write one or more record(s) as vCard(s) to file(s).
385 If \"\\[bbdb-apply-next-command-to-all-records]\\[bbdb-vcard-export]\"\
386 is used instead of simply \"\\[bbdb-vcard-export]\", then export all \
388 in the *BBDB* buffer. If used with prefix argument, store records
389 in individual files."
391 (let ((default-filename ; argument filename-or-directory
392 (bbdb-vcard-make-file-name (bbdb-current-record nil
)))
393 (all-records-p (bbdb-do-all-records-p)))
396 (if current-prefix-arg
397 (read-directory-name "Write vCard files to directory: "
398 bbdb-vcard-default-dir nil
42)
400 "Write vCards to file: "
401 bbdb-vcard-default-dir
403 (format-time-string "%Y-%m-%dT%H:%M.vcf" (current-time))))
404 (read-file-name "Write current record to vCard file: "
405 bbdb-vcard-default-dir nil nil default-filename
))
406 all-records-p
; argument all-records-p
407 current-prefix-arg
))) ; argument one-file-per-record-p
409 (let ((records (progn (set-buffer bbdb-buffer-name
)
410 (mapcar 'car bbdb-records
)))
411 used-up-basenames
) ; keep them unique
412 (if one-file-per-record-p
414 (dolist (record records
)
417 (bbdb-vcard-make-file-name record
419 (insert (bbdb-vcard-from record
))
420 (bbdb-vcard-write-buffer
421 (concat filename-or-directory basename
))
422 (push basename used-up-basenames
))))
423 (message "Wrote %d vCards to %s"
424 (length used-up-basenames
) filename-or-directory
))
425 (with-temp-buffer ; all visible BBDB records in one file
426 (dolist (record records
)
427 (insert (bbdb-vcard-from record
)))
428 (bbdb-vcard-write-buffer filename-or-directory
))))
429 (let ((vcard (bbdb-vcard-from (bbdb-current-record nil
)))) ; current record
432 (bbdb-vcard-write-buffer filename-or-directory
)))))
435 (defun bbdb-vcard-export-to-kill-ring (all-records-p)
436 "From Buffer *BBDB*, copy one or more record(s) as vCard(s) to the kill ring.
438 If \"\\[bbdb-apply-next-command-to-all-records]\
439 \\[bbdb-vcard-export-to-kill-ring]\"\
440 is used instead of simply \"\\[bbdb-vcard-export-to-kill-ring]\", \
441 then export all records currently in
443 (interactive (let ((all-records-p (bbdb-do-all-records-p)))
444 (list all-records-p
)))
446 (let ((records (progn (set-buffer bbdb-buffer-name
)
447 (mapcar 'car bbdb-records
))))
449 (dolist (record records
)
450 (kill-append (bbdb-vcard-from record
) nil
))
451 (message "Saved %d records as vCards" (length records
)))
452 (kill-new (bbdb-vcard-from (bbdb-current-record nil
)))
453 (message "Saved record as vCard")))
455 ;;;###autoload (define-key bbdb-mode-map [(v)] 'bbdb-vcard-export)
456 (define-key bbdb-mode-map
[(v)] 'bbdb-vcard-export
)
457 ;;;###autoload (define-key bbdb-mode-map [(V)] 'bbdb-vcard-export-to-kill-ring)
458 (define-key bbdb-mode-map
[(V)] 'bbdb-vcard-export-to-kill-ring
)
462 (defun bbdb-vcard-iterate-vcards (vcard-processor vcards
)
463 "Apply VCARD-PROCESSOR successively to each vCard in string VCARDS.
464 When VCARDS is nil, return nil. Otherwise, return t."
467 (goto-char (point-min))
468 ;; Change CRLF into CR if necessary, dealing with inconsistent line
470 (while (re-search-forward "\r\n" nil t
)
471 (replace-match "\n" nil nil nil
1))
472 (setf (buffer-string) (bbdb-vcard-unfold-lines (buffer-string)))
473 (goto-char (point-min))
474 (while (re-search-forward
475 "^\\([[:alnum:]-]*\\.\\)?*BEGIN:VCARD[\n[:print:][:cntrl:]]*?\\(^\\([[:alnum:]-]*\\.\\)?END:VCARD\\)"
477 (let ((vcard (match-string 0)))
478 (if (string= "3.0" (bbdb-vcard-version-of vcard
))
479 (funcall vcard-processor vcard
)
480 (funcall vcard-processor
; probably a v2.1 vCard
481 (bbdb-vcard-unfold-lines
482 (bbdb-vcard-convert-to-3.0 vcard
))))))))
484 (defun bbdb-vcard-version-of (vcard)
485 "Return version number string of VCARD."
488 (car (bbdb-vcard-values-of-type "version" "value"))))
490 (defun bbdb-vcard-import-vcard (vcard)
491 "Store VCARD (version 3.0) in BBDB.
492 Extend existing BBDB records where possible."
495 (let* ((raw-name (car (bbdb-vcard-values-of-type "N" "value" t t
)))
496 ;; Name suitable for storing in BBDB:
497 (name (bbdb-vcard-unescape-strings
498 (bbdb-vcard-unvcardize-name raw-name
)))
499 ;; Name to search for in BBDB now:
501 (when raw-name
(if (stringp raw-name
)
503 (concat (nth 1 raw-name
) ;given name
505 (nth 0 raw-name
))))) ; family name
506 ;; Additional names from prefixed types like A.N, B.N, etc.:
510 (bbdb-join (bbdb-vcard-unvcardize-name (cdr (assoc "value" n
)))
512 (bbdb-vcard-elements-of-type "N" nil t
)))
513 (vcard-formatted-names (bbdb-vcard-unescape-strings
514 (bbdb-vcard-values-of-type "FN" "value")))
516 (bbdb-vcard-unescape-strings
517 (bbdb-vcard-split-structured-text
518 (car (bbdb-vcard-values-of-type "NICKNAME" "value"))
520 ;; Company suitable for storing in BBDB:
522 (bbdb-vcard-unescape-strings
523 (bbdb-vcard-unvcardize-org
524 (car (bbdb-vcard-values-of-type "ORG" "value" t t
)))))
525 ;; Company to search for in BBDB now:
526 (org-to-search-for vcard-org
) ; sorry
527 ;; Email suitable for storing in BBDB:
528 (vcard-email (bbdb-vcard-values-of-type "EMAIL" "value"))
529 ;; Email to search for in BBDB now:
532 (concat "\\(" (bbdb-join vcard-email
"\\)\\|\\(") "\\)")))
533 ;; Phone numbers suitable for storing in BBDB:
535 (mapcar (lambda (tel)
536 (vector (bbdb-vcard-translate
537 (or (cdr (assoc "type" tel
)) ""))
538 (cdr (assoc "value" tel
))))
539 (bbdb-vcard-elements-of-type "TEL")))
540 ;; Phone numbers to search for in BBDB now:
544 (mapconcat (lambda (x) (elt x
1))
545 vcard-tels
"\\)\\|\\(")
549 (mapcar 'bbdb-vcard-unvcardize-adr
550 (bbdb-vcard-elements-of-type "ADR" nil t
)))
551 (vcard-url (car (bbdb-vcard-values-of-type "URL" "value" t
)))
552 (vcard-notes (bbdb-vcard-values-of-type "NOTE" "value"))
553 (raw-bday (car (bbdb-vcard-values-of-type "BDAY" "value" t
)))
554 ;; Birthday suitable for storing in BBDB (usable by org-mode):
555 (vcard-bday (when raw-bday
(concat raw-bday
" birthday")))
556 ;; Birthday to search for in BBDB now:
557 (bday-to-search-for vcard-bday
)
558 ;; Non-birthday anniversaries, probably exported by ourselves:
559 (vcard-x-bbdb-anniversaries
560 (bbdb-vcard-split-structured-text
561 (car (bbdb-vcard-values-of-type "X-BBDB-ANNIVERSARY" "value"))
563 (vcard-rev (car (bbdb-vcard-values-of-type "REV" "value")))
564 (vcard-categories (bbdb-vcard-values-of-type "CATEGORIES" "value"))
565 ;; The BBDB record to change:
566 (record-freshness-info "BBDB record changed:") ; default user info
569 ;; Try to find an existing one ...
570 ;; (a) try company and net and name:
571 (car (and bbdb-vcard-try-merge
572 (bbdb-vcard-search-intersection
575 org-to-search-for email-to-search-for
)))
576 ;; (b) try company and name:
577 (car (and bbdb-vcard-try-merge
578 (bbdb-vcard-search-intersection
579 (bbdb-records) name-to-search-for org-to-search-for
)))
580 ;; (c) try net and name; we may change company here:
581 (car (and bbdb-vcard-try-merge
582 (bbdb-vcard-search-intersection
584 name-to-search-for nil email-to-search-for
)))
585 ;; (d) try birthday and name; we may change company here:
586 (car (and bbdb-vcard-try-merge
587 (bbdb-vcard-search-intersection
589 name-to-search-for nil nil bday-to-search-for
)))
590 ;; (e) try phone and name; we may change company here:
591 (car (and bbdb-vcard-try-merge
592 (bbdb-vcard-search-intersection
594 name-to-search-for nil nil nil tel-to-search-for
)))
595 ;; No existing record found; make a fresh one:
596 (let ((fresh-record (make-vector bbdb-record-length nil
)))
597 (bbdb-record-set-cache fresh-record
598 (make-vector bbdb-cache-length nil
))
599 (if vcard-rev
; For fresh records,
600 (bbdb-record-putprop ; set creation-date from vcard-rev
601 fresh-record
'creation-date
602 (replace-regexp-in-string
603 "\\([0-9]\\{4\\}-[01][0-9]-[0-3][0-9]\\).*" "\\1"
605 (bbdb-invoke-hook 'bbdb-create-hook fresh-record
))
606 (setq record-freshness-info
"BBDB record added:") ; user info
608 (bbdb-akas (bbdb-record-aka bbdb-record
))
609 (bbdb-addresses (bbdb-record-addresses bbdb-record
))
610 (bbdb-phones (bbdb-record-phones bbdb-record
))
611 (bbdb-nets (bbdb-record-net bbdb-record
))
612 (bbdb-raw-notes (bbdb-record-raw-notes bbdb-record
))
615 (bbdb-vcard-elements-of-type "BEGIN") ; get rid of delimiter
616 (bbdb-vcard-elements-of-type "END") ; get rid of delimiter
617 (bbdb-vcard-elements-of-type "VERSION") ; get rid of this too
618 (when name
; which should be the case as N is mandatory in vCard
619 (bbdb-record-set-firstname bbdb-record
(car name
))
620 (bbdb-record-set-lastname bbdb-record
(cadr name
)))
623 (remove (concat (bbdb-record-firstname bbdb-record
)
624 " " (bbdb-record-lastname bbdb-record
))
625 (reduce (lambda (x y
) (union x y
:test
'string
=))
626 (list vcard-nicknames
628 vcard-formatted-names
630 (when vcard-org
(bbdb-record-set-company bbdb-record vcard-org
))
632 bbdb-record
(union vcard-email bbdb-nets
:test
'string
=))
633 (bbdb-record-set-addresses
634 bbdb-record
(union vcard-adrs bbdb-addresses
:test
'equal
))
635 (bbdb-record-set-phones bbdb-record
636 (union vcard-tels bbdb-phones
:test
'equal
))
637 ;; prepare bbdb's notes:
638 (when vcard-url
(push (cons 'www vcard-url
) bbdb-raw-notes
))
640 ;; Put vCard NOTEs under key 'notes (append if necessary).
641 (unless (assq 'notes bbdb-raw-notes
)
642 (push (cons 'notes
"") bbdb-raw-notes
))
643 (setf (cdr (assq 'notes bbdb-raw-notes
))
644 (bbdb-vcard-merge-strings
645 (cdr (assq 'notes bbdb-raw-notes
))
646 (bbdb-vcard-unescape-strings vcard-notes
)
648 (when (or vcard-bday vcard-x-bbdb-anniversaries
)
649 ;; Put vCard BDAY and vCard X-BBDB-ANNIVERSARY's under key
650 ;; 'anniversary (append if necessary) where org-mode can find it.
651 (when vcard-bday
(push vcard-bday vcard-x-bbdb-anniversaries
))
652 (unless (assq 'anniversary bbdb-raw-notes
)
653 (push (cons 'anniversary
"") bbdb-raw-notes
))
654 (setf (cdr (assq 'anniversary bbdb-raw-notes
))
655 (bbdb-vcard-merge-strings
656 (cdr (assq 'anniversary bbdb-raw-notes
))
657 (bbdb-vcard-unescape-strings vcard-x-bbdb-anniversaries
)
659 (when vcard-categories
660 ;; Put vCard CATEGORIES under key 'mail-alias (append if necessary).
661 (unless (assq 'mail-alias bbdb-raw-notes
)
662 (push (cons 'mail-alias
"") bbdb-raw-notes
))
663 (setf (cdr (assq 'mail-alias bbdb-raw-notes
))
664 (bbdb-vcard-merge-strings
665 (cdr (assq 'mail-alias bbdb-raw-notes
))
668 (while (setq other-vcard-type
(bbdb-vcard-other-element))
669 (when (string-match "^\\([[:alnum:]-]*\\.\\)?AGENT"
670 (symbol-name (car other-vcard-type
)))
671 ;; Notice other vCards inside the current one.
672 (bbdb-vcard-iterate-vcards
673 'bbdb-vcard-import-vcard
; needed for inner v2.1 vCards:
674 (replace-regexp-in-string "\\\\" "" (cdr other-vcard-type
))))
675 (unless (or (and bbdb-vcard-skip-on-import
676 (string-match bbdb-vcard-skip-on-import
677 (symbol-name (car other-vcard-type
))))
678 (and bbdb-vcard-skip-valueless
679 (zerop (length (cdr other-vcard-type
)))))
680 (push (bbdb-vcard-remove-x-bbdb other-vcard-type
) bbdb-raw-notes
)))
681 (bbdb-record-set-raw-notes
683 (remove-duplicates bbdb-raw-notes
:test
'equal
:from-end t
))
684 (bbdb-change-record bbdb-record t
)
685 ;; Tell the user what we've done.
686 (message "%s %s %s -- %s"
687 record-freshness-info
688 (bbdb-record-firstname bbdb-record
)
689 (bbdb-record-lastname bbdb-record
)
690 (replace-regexp-in-string
691 "\n" "; " (or (bbdb-record-company bbdb-record
) "-"))))))
693 (defun bbdb-vcard-from (record)
694 "Return BBDB RECORD as a vCard."
696 (let* ((name (bbdb-record-name record
))
697 (first-name (bbdb-record-firstname record
))
698 (last-name (bbdb-record-lastname record
))
699 (aka (bbdb-record-aka record
))
700 (company (bbdb-record-company record
))
701 (net (bbdb-record-net record
))
702 (phones (bbdb-record-phones record
))
703 (addresses (bbdb-record-addresses record
))
704 (www (bbdb-get-field record
'www
))
706 (bbdb-vcard-split-structured-text (bbdb-record-notes record
)
708 (raw-anniversaries (bbdb-vcard-split-structured-text
709 (bbdb-get-field record
'anniversary
) "\n" t
))
711 "\\([0-9]\\{4\\}-[01][0-9]-[0-3][0-9]\\)\\([[:blank:]]+birthday\\)?\\'")
713 (car (bbdb-vcard-split-structured-text
714 (find-if (lambda (x) (string-match birthday-regexp x
))
718 (remove-if (lambda (x) (string-match birthday-regexp x
))
719 raw-anniversaries
:count
1))
720 (creation-date (bbdb-get-field record
'creation-date
))
721 (mail-aliases (bbdb-record-getprop record
722 bbdb-define-all-aliases-field
))
723 (raw-notes (copy-alist (bbdb-record-raw-notes record
))))
724 (bbdb-vcard-insert-vcard-element "BEGIN" "VCARD")
725 (bbdb-vcard-insert-vcard-element "VERSION" "3.0")
726 (bbdb-vcard-insert-vcard-element "FN" (bbdb-vcard-escape-strings name
))
727 (bbdb-vcard-insert-vcard-element
728 "N" (bbdb-vcard-escape-strings last-name
)
729 ";" (bbdb-vcard-escape-strings first-name
)
730 ";;;") ; Additional Names, Honorific Prefixes, Honorific Suffixes
731 (bbdb-vcard-insert-vcard-element
732 "NICKNAME" (bbdb-join (bbdb-vcard-escape-strings aka
) ","))
733 (bbdb-vcard-insert-vcard-element
734 "ORG" (bbdb-vcard-escape-strings company
))
736 (bbdb-vcard-insert-vcard-element
737 "EMAIL;TYPE=INTERNET" (bbdb-vcard-escape-strings mail
)))
738 (dolist (phone phones
)
739 (bbdb-vcard-insert-vcard-element
742 (bbdb-vcard-escape-strings
743 (bbdb-vcard-translate (bbdb-phone-location phone
) t
)))
744 (bbdb-vcard-escape-strings (bbdb-phone-string phone
))))
745 (dolist (address addresses
)
746 (bbdb-vcard-insert-vcard-element
749 (bbdb-vcard-escape-strings
750 (bbdb-vcard-translate (bbdb-address-location address
) t
)))
751 ";;" ; no Postbox, no Extended
752 (bbdb-join (bbdb-vcard-escape-strings (bbdb-address-streets address
))
754 ";" (bbdb-vcard-vcardize-address-element
755 (bbdb-vcard-escape-strings (bbdb-address-city address
)))
756 ";" (bbdb-vcard-vcardize-address-element
757 (bbdb-vcard-escape-strings (bbdb-address-state address
)))
758 ";" (bbdb-vcard-vcardize-address-element
759 (bbdb-vcard-escape-strings (bbdb-address-zip address
)))
760 ";" (bbdb-vcard-vcardize-address-element
761 (bbdb-vcard-escape-strings (bbdb-address-country address
)))))
762 (bbdb-vcard-insert-vcard-element "URL" www
)
764 (bbdb-vcard-insert-vcard-element
765 "NOTE" (bbdb-vcard-escape-strings note
)))
766 (bbdb-vcard-insert-vcard-element "BDAY" birthday
)
767 (bbdb-vcard-insert-vcard-element ; non-birthday anniversaries
768 "X-BBDB-ANNIVERSARY" (bbdb-join other-anniversaries
"\\n"))
769 (bbdb-vcard-insert-vcard-element "REV" creation-date
)
770 (bbdb-vcard-insert-vcard-element
772 (bbdb-join (bbdb-vcard-escape-strings
773 (bbdb-vcard-split-structured-text mail-aliases
"," t
)) ","))
774 ;; prune raw-notes...
775 (dolist (key '(www notes anniversary mail-alias creation-date timestamp
))
776 (setq raw-notes
(assq-delete-all key raw-notes
)))
777 ;; ... and output what's left
778 (dolist (raw-note raw-notes
)
779 (bbdb-vcard-insert-vcard-element
780 (symbol-name (bbdb-vcard-prepend-x-bbdb-maybe (car raw-note
)))
781 (bbdb-vcard-escape-strings (cdr raw-note
))))
782 (bbdb-vcard-insert-vcard-element "END" "VCARD")
783 (bbdb-vcard-insert-vcard-element nil
)) ; newline
788 (defun bbdb-vcard-convert-to-3.0
(vcard)
789 "Convert VCARD from v2.1 to v3.0.
790 Return a version 3.0 vCard as a string. Don't bother about the vCard
791 v3.0 mandatory elements N and FN."
792 ;; Prevent customization of vcard.el's from being changed behind our back:
793 (let ((vcard-standard-filters '(vcard-filter-html)))
795 (bbdb-vcard-insert-vcard-element "BEGIN" "VCARD")
796 (bbdb-vcard-insert-vcard-element "VERSION" "3.0")
797 (dolist (element (remove*
798 "VERSION" (vcard-parse-string vcard
)
799 :key
(lambda (x) (upcase (caar x
))) :test
'string
=))
800 (bbdb-vcard-insert-vcard-element
801 (concat (caar element
)
802 (mapconcat 'bbdb-vcard-parameter-pair
(cdar element
) ""))
803 (bbdb-join (bbdb-vcard-escape-strings (cdr element
)) ";")))
804 (bbdb-vcard-insert-vcard-element "END" "VCARD")
805 (bbdb-vcard-insert-vcard-element nil
)
808 (defun bbdb-vcard-parameter-pair (input)
809 "Return \"parameter=value\" made from INPUT.
810 INPUT is its representation in vcard.el. Return empty string if INPUT
812 (cond ((consp input
) (concat ";" (car input
) "=" (cdr input
)))
813 ((stringp input
) (concat ";TYPE=" input
))
818 (defun bbdb-vcard-values-of-type
819 (type parameter
&optional one-is-enough-p split-value-at-semi-colon-p
)
820 "Return in a list the values of PARAMETER of vCard element of TYPE.
821 The VCard element is read and deleted from current buffer which is
822 supposed to contain a single vCard. If ONE-IS-ENOUGH-P is non-nil,
823 read and delete only the first element of TYPE. If PARAMETER is
824 \"value\" and SPLIT-VALUE-AT-SEMI-COLON-P is non-nil, split the value
825 at semi-colons into a list."
826 (mapcar (lambda (x) (cdr (assoc parameter x
)))
827 (bbdb-vcard-elements-of-type
828 type one-is-enough-p split-value-at-semi-colon-p
)))
830 (defun bbdb-vcard-elements-of-type
831 (type &optional one-is-enough-p split-value-at-semi-colon-p
)
832 "From current buffer read and delete the vCard elements of TYPE.
833 The current buffer is supposed to contain a single vCard. If
834 ONE-IS-ENOUGH-P is non-nil, read and delete only the first element of
835 TYPE. Return a list of alists, one per element. Each alist has a
836 cell with key \"value\" containing the element's value, and may have
837 other elements of the form \(parameter-name . parameter-value). If
838 SPLIT-VALUE-AT-SEMI-COLON-P is non-nil, split the value at key
839 \"value\" at semi-colons into a list."
840 (goto-char (point-min))
841 (let (values parameters read-enough
)
847 "^\\([[:alnum:]-]*\\.\\)?\\(" type
"\\)\\(;.*\\)?:\\(.*\\)$")
849 (goto-char (match-end 2))
850 (setq parameters nil
)
851 (push (cons "value" (if split-value-at-semi-colon-p
852 (bbdb-vcard-split-structured-text
853 (match-string 4) ";")
856 (while (re-search-forward "\\([^;:=]+\\)=\\([^;:]+\\)"
857 (line-end-position) t
)
858 (let* ((parameter-key (downcase (match-string 1)))
859 (parameter-value (downcase (match-string 2)))
860 (parameter-sibling (assoc parameter-key parameters
)))
861 (if parameter-sibling
; i.e., pair with equal key
862 ;; collect vCard parameter list `;a=x;a=y;a=z'
863 ;; into vCard value list `;a=x,y,z'; becoming ("a" . "x,y,z")
864 (setf (cdr parameter-sibling
)
865 (concat (cdr parameter-sibling
) "," parameter-value
))
866 ;; vCard parameter pair `;key=value;' with new key
867 (push (cons parameter-key parameter-value
) parameters
))))
868 (push parameters values
)
869 (delete-region (line-end-position 0) (line-end-position))
870 (when one-is-enough-p
(setq read-enough t
)))
873 (defun bbdb-vcard-other-element ()
874 "From current buffer read and delete the topmost vCard element.
875 Buffer is supposed to contain a single vCard. Return (TYPE . VALUE)."
876 (goto-char (point-min))
877 (when (re-search-forward "^\\([[:graph:]]*?\\):\\(.*\\)$" nil t
)
878 (let ((type (match-string 1))
879 (value (match-string 2)))
880 (delete-region (match-beginning 0) (match-end 0))
881 (cons (intern (downcase type
)) (bbdb-vcard-unescape-strings value
)))))
883 (defun bbdb-vcard-insert-vcard-element (type &rest values
)
884 "Insert a vCard element comprising TYPE, `:', VALUES into current buffer.
885 Take care of TYPE canonicalization, line folding, and closing newline.
886 Do nothing if TYPE is non-nil and VALUES are empty. Insert just a
887 newline if TYPE is nil."
889 (let ((value (bbdb-join values
"")))
890 (unless (zerop (length value
))
891 (insert (bbdb-vcard-fold-line
892 (concat (bbdb-vcard-canonicalize-vcard-type type
)
894 (insert (bbdb-vcard-fold-line ""))))
898 (defun bbdb-vcard-unfold-lines (vcards)
899 "Return folded vCard lines from VCARDS unfolded."
900 (replace-regexp-in-string "\n\\( \\|\t\\)" "" vcards
))
902 (defun bbdb-vcard-fold-line (long-line)
903 "Insert after every 75th position in LONG-LINE a newline and a space."
904 (with-temp-buffer (insert long-line
)
905 (goto-char (point-min))
906 (while (< (goto-char (+ (point) 75))
912 (defun bbdb-vcard-unescape-strings (escaped-strings)
913 "Unescape escaped `;', `,', `\\', and newlines in ESCAPED-STRINGS.
914 ESCAPED-STRINGS may be a string or a sequence of strings."
915 (flet ((unescape (x) (replace-regexp-in-string
916 "\\([\\\\]\\)\\([,;\\]\\)" ""
917 (replace-regexp-in-string "\\\\n" "\n" x
)
919 (bbdb-vcard-process-strings 'unescape escaped-strings
)))
921 (defun bbdb-vcard-escape-strings (unescaped-strings )
922 "Escape `;', `,', `\\', and newlines in UNESCAPED-STRINGS.
923 UNESCAPED-STRINGS may be a string or a sequence of strings."
924 (flet ((escape (x) (replace-regexp-in-string ; from 2.1 conversion:
925 "\r" "" (replace-regexp-in-string
926 "\n" "\\\\n" (replace-regexp-in-string
927 "\\(\\)[,;\\]" "\\\\" (or x
"")
929 (bbdb-vcard-process-strings 'escape unescaped-strings
)))
931 (defun bbdb-vcard-process-strings (string-processor strings
)
932 "Apply STRING-PROCESSOR to STRINGS.
933 STRINGS may be a string or a sequence of strings."
934 (if (stringp strings
)
935 (funcall string-processor strings
)
936 (mapcar string-processor strings
)))
940 (defun bbdb-vcard-remove-x-bbdb (vcard-element)
941 "Remove the `X-BBDB-' prefix from the type part of VCARD-ELEMENT if any."
942 (cons (intern (replace-regexp-in-string
943 "^X-BBDB-" "" (symbol-name (car vcard-element
))))
944 (cdr vcard-element
)))
946 (defun bbdb-vcard-prepend-x-bbdb-maybe (bbdb-fieldname)
947 "If BBDB-FIELDNAME is in `bbdb-vcard-x-bbdb-candidates', prepend `X-BBDB'."
948 (if (member bbdb-fieldname bbdb-vcard-x-bbdb-candidates
)
949 (intern (concat "x-bbdb-" (symbol-name bbdb-fieldname
)))
950 bbdb-fieldname
)) ; lowercase more consistent here
952 (defun bbdb-vcard-unvcardize-name (vcard-name)
953 "Convert VCARD-NAME (type N) into (FIRSTNAME LASTNAME)."
954 (if (stringp vcard-name
) ; unstructured N
955 (bbdb-divide-name vcard-name
)
958 (bbdb-join (bbdb-vcard-split-structured-text x
"," t
)
960 vcard-name
))) ; flatten comma-separated substructure
961 (list (concat (nth 3 vcard-name
) ; honorific prefixes
962 (unless (zerop (length (nth 3 vcard-name
))) " ")
963 (nth 1 vcard-name
) ; given name
964 (unless (zerop (length (nth 2 vcard-name
))) " ")
965 (nth 2 vcard-name
)) ; additional names
966 (concat (nth 0 vcard-name
) ; family name
967 (unless (zerop (length (nth 4 vcard-name
))) " ")
968 (nth 4 vcard-name
)))))) ; honorific suffixes
970 (defun bbdb-vcard-unvcardize-org (vcard-org)
971 "Convert VCARD-ORG (type ORG), which may be a list, into a string."
972 (if (or (null vcard-org
)
973 (stringp vcard-org
)) ; unstructured, probably non-standard ORG
974 vcard-org
; Company, unit 1, unit 2...
975 (bbdb-join vcard-org
"\n")))
977 (defun bbdb-vcard-unvcardize-adr (vcard-adr)
978 "Convert VCARD-ADR into BBDB format.
979 Turn a vCard element of type ADR into (TYPE STREETS CITY STATE ZIP
981 (let ((adr-type (or (cdr (assoc "type" vcard-adr
)) ""))
982 (streets ; all comma-separated sub-elements of
983 (remove ; Postbox, Extended, Streets go into one list
986 (bbdb-vcard-split-structured-text x
"," t
))
987 (subseq (cdr (assoc "value" vcard-adr
))
989 (non-streets ; turn comma-separated substructure into
990 (mapcar ; newline-separated text
991 (lambda (x) (bbdb-join
992 (bbdb-vcard-split-structured-text x
"," t
)
994 (subseq (cdr (assoc "value" vcard-adr
))
996 (vector (bbdb-vcard-translate adr-type
)
998 (or (elt non-streets
0) "") ; City
999 (or (elt non-streets
1) "") ; State
1000 (or (elt non-streets
2) "") ; Zip
1001 (or (elt non-streets
3) "")))) ; Country
1003 (defun bbdb-vcard-vcardize-address-element (address-element)
1004 "Replace escaped newlines in ADDRESS-ELEMENT by commas."
1005 (replace-regexp-in-string "\\\\n" "," address-element
))
1007 (defun bbdb-vcard-translate (label &optional exportp
)
1008 "Translate LABEL from vCard to BBDB or, if EXPORTP is non-nil, vice versa.
1009 Translations are defined in `bbdb-vcard-import-translation-table' and
1010 `bbdb-vcard-export-translation-table' respectively."
1013 (or (assoc-default label
1015 bbdb-vcard-export-translation-table
1016 bbdb-vcard-import-translation-table
) 'string-match
)
1019 (defun bbdb-vcard-merge-strings (old-string new-strings separator
)
1020 "Merge strings successively from list NEW-STRINGS into OLD-STRING.
1021 If an element of NEW-STRINGS is already in OLD-STRING, leave
1022 OLD-STRING unchanged. Otherwise append SEPARATOR and NEW-STRING."
1025 (dolist (new-string new-strings
)
1026 (unless (prog1 (search-backward new-string nil t
)
1027 (goto-char (point-max)))
1028 (unless (zerop (buffer-size)) (insert separator
))
1029 (insert new-string
)))
1032 (defun bbdb-vcard-split-structured-text
1033 (text separator
&optional return-always-list-p
)
1034 "Split TEXT at unescaped occurrences of SEPARATOR; return parts in a list.
1035 Return text unchanged if there aren't any separators and RETURN-ALWAYS-LIST-P
1037 (when (stringp text
)
1038 (let ((string-elements
1040 (replace-regexp-in-string
1041 (concat "\\\\\r" separator
) (concat "\\\\" separator
)
1042 (replace-regexp-in-string separator
(concat "\r" separator
) text
))
1043 (concat "\r" separator
))))
1044 (if (and (null return-always-list-p
)
1045 (= 1 (length string-elements
)))
1046 (car string-elements
)
1049 (defun bbdb-vcard-canonicalize-vcard-type (&rest strings
)
1050 "Concatenate STRINGS and apply `bbdb-vcard-type-canonicalizer' to them."
1051 (funcall bbdb-vcard-type-canonicalizer
(bbdb-join strings
"")))
1053 (defun bbdb-vcard-write-buffer (vcard-file-name)
1054 "Write current buffer to VCARD-FILE-NAME.
1055 Create directories where necessary."
1056 (make-directory (file-name-directory vcard-file-name
) t
)
1057 (let ((buffer-file-coding-system bbdb-vcard-export-coding-system
))
1058 (write-region nil nil vcard-file-name nil nil nil t
)))
1060 (defun bbdb-vcard-make-file-name (bbdb-record &optional used-up-basenames
)
1061 "Come up with a vCard filename given a BBDB-RECORD.
1062 Make it unique against the list USED-UP-BASENAMES."
1063 (let ((name (bbdb-record-name bbdb-record
))
1064 (aka (car (bbdb-record-aka bbdb-record
)))
1070 (replace-regexp-in-string
1072 (or (unless (zerop (length name
)) name
)
1073 (unless (zerop (length aka
)) aka
)
1075 (unless (zerop unique-number
)
1076 (concat "-" (number-to-string unique-number
)))
1079 (incf unique-number
))
1082 (defmacro bbdb-vcard-search-intersection
1083 (records &optional name company net notes phone
)
1084 "Search RECORDS for records that match each non-nil argument."
1087 (if phone
`(when ,phone
(bbdb-search ,records nil nil nil nil
,phone
))
1090 (if notes
`(when ,notes
(bbdb-search ,phone-search nil nil nil
,notes
))
1093 (if net
`(when ,net
(bbdb-search ,notes-search nil nil
,net
))
1096 (if company
`(when ,company
(bbdb-search ,net-search nil
,company
))
1099 (if name
`(when ,name
(bbdb-search ,company-search
,name
))
1105 (provide 'bbdb-vcard
)
1107 ;;; bbdb-vcard.el ends here