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. Time
117 ;; and time zone information from REV are stored there as well if
118 ;; there are any, but are ignored by BBDB (v2.36).
120 ;; VCard type prefixes (A.ADR:..., B.ADR:... etc.) are stripped off
121 ;; and discarded from the following types: N, FN, NICKNAME, ORG (first
122 ;; occurrence), ADR, TEL, EMAIL, URL, BDAY (first occurrence), NOTE.
124 ;; VCard types that are prefixed `X-BBDB-' are stored in BBDB without
127 ;; VCard type X-BBDB-ANNIVERSARY may contain (previously exported)
128 ;; newline-separated non-birthday anniversaries that are meant to be
131 ;; All remaining vCard types that don't match the regexp in
132 ;; `bbdb-vcard-skip-on-import' and that have a non-empty value are
133 ;; stored unaltered in the BBDB Notes alist where, for instance,
134 ;; `TZ;VALUE=text:-05:00' is stored as `(tz\;value=text . "-05:00")'.
135 ;; From the BBDB data fields AKA, Phones, Addresses, Net Addresses,
136 ;; and Notes, duplicates are removed, respectively.
138 ;; VCards found inside other vCards (as values of type AGENT) are
142 ;; Handling of the individual types defined in RFC2426 during import
143 ;; (assuming default label translation and no vCard type exclusion):
145 ;; |----------------------+----------------------------------------|
146 ;; | VCARD TYPE; | STORAGE IN BBDB |
148 ;; |----------------------+----------------------------------------|
150 ;; |----------------------+----------------------------------------|
151 ;; | N | First occurrence: |
156 ;; | | AKAs (append) |
157 ;; |----------------------+----------------------------------------|
158 ;; | FN | AKAs (append) |
159 ;; | NICKNAME | AKAs (append) |
160 ;; |----------------------+----------------------------------------|
161 ;; | ORG | First occurrence: |
166 ;; | | (repeatedly) |
167 ;; |----------------------+----------------------------------------|
168 ;; | ADR;TYPE=x,HOME,y | Addresses<Home |
169 ;; | ADR;TYPE=x;TYPE=HOME | Addresses<Home |
170 ;; | ADR;TYPE=x,WORK,y | Addresses<Office |
171 ;; | ADR;TYPE=x;TYPE=WORK | Addresses<Office |
172 ;; | ADR;TYPE=x,y,z | Addresses<x,y,z |
173 ;; | ADR;TYPE=x;TYPE=y | Addresses<x,y |
174 ;; | ADR | Addresses<Office |
175 ;; |----------------------+----------------------------------------|
176 ;; | TEL;TYPE=x,HOME,y | Phones<Home (append) |
177 ;; | TEL;TYPE=x;TYPE=HOME | Phones<Home (append) |
178 ;; | TEL;TYPE=x,WORK,y | Phones<Office (append) |
179 ;; | TEL;TYPE=x;TYPE=WORK | Phones<Office (append) |
180 ;; | TEL;TYPE=x,CELL,y | Phones<Mobile (append) |
181 ;; | TEL;TYPE=x;TYPE=CELL | Phones<Mobile (append) |
182 ;; | TEL;TYPE=x,y,z | Phones<x,y,z (append) |
183 ;; | TEL;TYPE=x;TYPE=y | Phones<x,y (append) |
184 ;; | TEL | Phones<Office (append) |
185 ;; |----------------------+----------------------------------------|
186 ;; | EMAIL;TYPE=x,y,z | Net-Addresses (append) |
187 ;; | URL | Notes<www |
188 ;; |----------------------+----------------------------------------|
189 ;; | BDAY | Notes<anniversary (append as birthday) |
190 ;; | X-BBDB-ANNIVERSARY | Notes<anniversary (append) |
191 ;; |----------------------+----------------------------------------|
192 ;; | NOTE | Notes<notes (append) |
193 ;; | REV | Notes<creation-date |
194 ;; | CATEGORIES | Notes<mail-alias (append) |
195 ;; | SORT-STRING | Notes<sort-string |
196 ;; | KEY | Notes<key |
197 ;; | GEO | Notes<geo |
199 ;; | PHOTO | Notes<photo |
200 ;; | LABEL | Notes<label |
201 ;; | LOGO | Notes<logo |
202 ;; | SOUND | Notes<sound |
203 ;; | TITLE | Notes<title |
204 ;; | ROLE | Notes<role |
205 ;; | AGENT | Notes<agent |
206 ;; | MAILER | Notes<mailer |
207 ;; | UID | Notes<uid |
208 ;; | PRODID | Notes<prodid |
209 ;; | CLASS | Notes<class |
210 ;; | X-foo | Notes<x-foo |
211 ;; | X-BBDB-bar | Notes<bar |
212 ;; |----------------------+----------------------------------------|
213 ;; | anyJunK;a=x;b=y | Notes<anyjunk;a=x;b=y |
214 ;; |----------------------+----------------------------------------|
220 ;; VCard types N (only fields lastname, firstname) and FN both come
223 ;; Members of BBDB field AKA are stored comma-separated under the
224 ;; vCard type NICKNAME.
226 ;; Labels of Addresses and Phones are translated as defined in
227 ;; `bbdb-vcard-export-translation-table' into type parameters of
228 ;; vCard types ADR and TEL, respectively.
230 ;; In vCard type ADR, fields postbox and extended address are always
231 ;; empty. Newlines which subdivide BBDB Address fields are converted
232 ;; into commas subdividing vCard ADR fields.
234 ;; The value of 'anniversary in Notes is supposed to be subdivided by
235 ;; newlines. The birthday part (either just a date or a date followed
236 ;; by \"birthday\") is stored under vCard type BDAY. The rest is
237 ;; stored newline-separated in the non-standard vCard type
238 ;; X-BBDB-ANNIVERSARY.
240 ;; Field names listed in `bbdb-vcard-x-bbdb-candidates' are in the
241 ;; exported vCard prepended by `X-BBDB-'.
243 ;; The creation-date of the BBDB record is stored as vCard type REV.
245 ;; Remaining members of BBDB Notes are exported to the vCard without
261 (defconst bbdb-vcard-version
"0.3"
262 "Version of the vCard importer/exporter.
263 The major part increases on user-visible changes.")
269 (defgroup bbdb-vcard nil
270 "Customizations for vCards"
273 (defcustom bbdb-vcard-skip-on-import
"X-GSM-"
274 "Regexp describing vCard elements that are to be discarded during import.
275 Example: `X-GSM-\\|X-MS-'."
279 (defcustom bbdb-vcard-skip-valueless t
280 "Skip vCard element types with an empty value.
281 Nil means insert empty types into BBDB."
285 (defcustom bbdb-vcard-import-translation-table
286 '(("CELL\\|CAR" .
"Mobile")
288 ("HOME" .
"Home") ; translates e.g. "dom,home,postal,parcel" to "Home"
289 ("^$" .
"Office")) ; acts as a default for parameterless ADR or TEL
290 "Label translation on vCard import.
291 Alist with translations of location labels for addresses and phone
292 numbers. Cells are (VCARD-LABEL-REGEXP . BBDB-LABEL). One entry
293 should map a default BBDB label to the empty string (`\"^$\"') which
294 corresponds to unlabelled vCard elements."
296 :type
'(alist :key-type
297 (choice regexp
(const :tag
"Empty (as default)" "^$"))
300 (defcustom bbdb-vcard-try-merge t
301 "Try to merge vCards into existing BBDB records.
302 Nil means create a fresh bbdb record each time a vCard is read."
306 (defcustom bbdb-vcard-type-canonicalizer
'upcase
307 "Function to apply to vCard type names on export.
308 Most reasonable choices are `upcase' and `downcase'."
312 (defcustom bbdb-vcard-x-bbdb-candidates
320 aka
) ; not sure what this is for
321 "List of translatable BBDB user field names.
322 On export to a vCard, they are transformed into vCard-compliant
323 extended types by prepending `X-BBDB-'. On (re-)import, this prefix
326 :type
'(repeat symbol
))
328 (defcustom bbdb-vcard-export-translation-table
329 '(("Mobile" .
"CELL")
331 "Label translation on vCard export.
332 Alist with translations of location labels for addresses and phone
333 numbers. Cells are (BBDB-LABEL-REGEXP . VCARD-LABEL)."
335 :type
'(alist :key-type
336 (choice regexp
(const :tag
"Empty (as default)" "^$"))
339 (defcustom bbdb-vcard-export-coding-system
340 'utf-8-dos
; dos line endings mandatory according to RFC 2426
341 "Coding system to use when writing vCard files."
345 (defcustom bbdb-vcard-default-dir
"~/exported-vcards/"
346 "Default storage directory for exported vCards.
347 Nil means current directory."
349 :type
'(choice directory
(const :tag
"Current directory" nil
)))
356 (defun bbdb-vcard-import-region (begin end
)
357 "Import the vCards between BEGIN and END into BBDB.
358 Existing BBDB records may be altered."
360 (bbdb-vcard-iterate-vcards 'bbdb-vcard-import-vcard
361 (buffer-substring-no-properties begin end
)))
364 (defun bbdb-vcard-import-buffer (vcard-buffer)
365 "Import vCards from VCARD-BUFFER into BBDB.
366 Existing BBDB records may be altered."
367 (interactive (list (current-buffer)))
368 (set-buffer vcard-buffer
)
369 (bbdb-vcard-import-region (point-min) (point-max)))
372 (defun bbdb-vcard-import-file (vcard-file)
373 "Import vCards from VCARD-FILE into BBDB.
374 If VCARD-FILE is a wildcard, import each matching file. Existing BBDB
375 records may be altered."
376 (interactive "FvCard file (or wildcard): ")
377 (dolist (vcard-file (file-expand-wildcards vcard-file
))
379 (insert-file-contents vcard-file
)
380 (bbdb-vcard-import-region (point-min) (point-max)))))
383 (defun bbdb-vcard-export
384 (filename-or-directory all-records-p one-file-per-record-p
)
385 "From Buffer *BBDB*, write one or more record(s) as vCard(s) to file(s).
387 If \"\\[bbdb-apply-next-command-to-all-records]\\[bbdb-vcard-export]\"\
388 is used instead of simply \"\\[bbdb-vcard-export]\", then export all \
390 in the *BBDB* buffer. If used with prefix argument, store records
391 in individual files."
393 (let ((default-filename ; argument filename-or-directory
394 (bbdb-vcard-make-file-name (bbdb-current-record nil
)))
395 (all-records-p (bbdb-do-all-records-p)))
398 (if current-prefix-arg
399 (read-directory-name "Write vCard files to directory: "
400 bbdb-vcard-default-dir nil
42)
402 "Write vCards to file: "
403 bbdb-vcard-default-dir
405 (format-time-string "%Y-%m-%dT%H:%M.vcf" (current-time))))
406 (read-file-name "Write current record to vCard file: "
407 bbdb-vcard-default-dir nil nil default-filename
))
408 all-records-p
; argument all-records-p
409 current-prefix-arg
))) ; argument one-file-per-record-p
411 (let ((records (progn (set-buffer bbdb-buffer-name
)
412 (mapcar 'car bbdb-records
)))
413 used-up-basenames
) ; keep them unique
414 (if one-file-per-record-p
416 (dolist (record records
)
419 (bbdb-vcard-make-file-name record
421 (insert (bbdb-vcard-from record
))
422 (bbdb-vcard-write-buffer
423 (concat filename-or-directory basename
))
424 (push basename used-up-basenames
))))
425 (message "Wrote %d vCards to %s"
426 (length used-up-basenames
) filename-or-directory
))
427 (with-temp-buffer ; all visible BBDB records in one file
428 (dolist (record records
)
429 (insert (bbdb-vcard-from record
)))
430 (bbdb-vcard-write-buffer filename-or-directory
))))
431 (let ((vcard (bbdb-vcard-from (bbdb-current-record nil
)))) ; current record
434 (bbdb-vcard-write-buffer filename-or-directory
)))))
437 (defun bbdb-vcard-export-to-kill-ring (all-records-p)
438 "From Buffer *BBDB*, copy one or more record(s) as vCard(s) to the kill ring.
440 If \"\\[bbdb-apply-next-command-to-all-records]\
441 \\[bbdb-vcard-export-to-kill-ring]\"\
442 is used instead of simply \"\\[bbdb-vcard-export-to-kill-ring]\", \
443 then export all records currently in
445 (interactive (let ((all-records-p (bbdb-do-all-records-p)))
446 (list all-records-p
)))
448 (let ((records (progn (set-buffer bbdb-buffer-name
)
449 (mapcar 'car bbdb-records
))))
451 (dolist (record records
)
452 (kill-append (bbdb-vcard-from record
) nil
))
453 (message "Saved %d records as vCards" (length records
)))
454 (kill-new (bbdb-vcard-from (bbdb-current-record nil
)))
455 (message "Saved record as vCard")))
457 ;;;###autoload (define-key bbdb-mode-map [(v)] 'bbdb-vcard-export)
458 (define-key bbdb-mode-map
[(v)] 'bbdb-vcard-export
)
459 ;;;###autoload (define-key bbdb-mode-map [(V)] 'bbdb-vcard-export-to-kill-ring)
460 (define-key bbdb-mode-map
[(V)] 'bbdb-vcard-export-to-kill-ring
)
464 (defun bbdb-vcard-iterate-vcards (vcard-processor vcards
)
465 "Apply VCARD-PROCESSOR successively to each vCard in string VCARDS.
466 When VCARDS is nil, return nil. Otherwise, return t."
469 (goto-char (point-min))
470 ;; Change CRLF into CR if necessary, dealing with inconsistent line
472 (while (re-search-forward "\r\n" nil t
)
473 (replace-match "\n" nil nil nil
1))
474 (setf (buffer-string) (bbdb-vcard-unfold-lines (buffer-string)))
475 (goto-char (point-min))
476 (while (re-search-forward
477 "^\\([[:alnum:]-]*\\.\\)?*BEGIN:VCARD[\n[:print:][:cntrl:]]*?\\(^\\([[:alnum:]-]*\\.\\)?END:VCARD\\)"
479 (let ((vcard (match-string 0)))
480 (if (string= "3.0" (bbdb-vcard-version-of vcard
))
481 (funcall vcard-processor vcard
)
482 (funcall vcard-processor
; probably a v2.1 vCard
483 (bbdb-vcard-unfold-lines
484 (bbdb-vcard-convert-to-3.0 vcard
))))))))
486 (defun bbdb-vcard-version-of (vcard)
487 "Return version number string of VCARD."
490 (car (bbdb-vcard-values-of-type "version" "value"))))
492 (defun bbdb-vcard-import-vcard (vcard)
493 "Store VCARD (version 3.0) in BBDB.
494 Extend existing BBDB records where possible."
497 (let* ((raw-name (car (bbdb-vcard-values-of-type "N" "value" t t
)))
498 ;; Name suitable for storing in BBDB:
499 (name (bbdb-vcard-unescape-strings
500 (bbdb-vcard-unvcardize-name raw-name
)))
501 ;; Name to search for in BBDB now:
503 (when raw-name
(if (stringp raw-name
)
505 (concat (nth 1 raw-name
) ;given name
507 (nth 0 raw-name
))))) ; family name
508 ;; Additional names from prefixed types like A.N, B.N, etc.:
512 (bbdb-join (bbdb-vcard-unvcardize-name (cdr (assoc "value" n
)))
514 (bbdb-vcard-elements-of-type "N" nil t
)))
515 (vcard-formatted-names (bbdb-vcard-unescape-strings
516 (bbdb-vcard-values-of-type "FN" "value")))
518 (bbdb-vcard-unescape-strings
519 (bbdb-vcard-split-structured-text
520 (car (bbdb-vcard-values-of-type "NICKNAME" "value"))
522 ;; Company suitable for storing in BBDB:
524 (bbdb-vcard-unescape-strings
525 (bbdb-vcard-unvcardize-org
526 (car (bbdb-vcard-values-of-type "ORG" "value" t t
)))))
527 ;; Company to search for in BBDB now:
528 (org-to-search-for vcard-org
) ; sorry
529 ;; Email suitable for storing in BBDB:
530 (vcard-email (bbdb-vcard-values-of-type "EMAIL" "value"))
531 ;; Email to search for in BBDB now:
534 (concat "\\(" (bbdb-join vcard-email
"\\)\\|\\(") "\\)")))
535 ;; Phone numbers suitable for storing in BBDB:
537 (mapcar (lambda (tel)
538 (vector (bbdb-vcard-translate
539 (or (cdr (assoc "type" tel
)) ""))
540 (cdr (assoc "value" tel
))))
541 (bbdb-vcard-elements-of-type "TEL")))
542 ;; Phone numbers to search for in BBDB now:
546 (mapconcat (lambda (x) (elt x
1))
547 vcard-tels
"\\)\\|\\(")
551 (mapcar 'bbdb-vcard-unvcardize-adr
552 (bbdb-vcard-elements-of-type "ADR" nil t
)))
553 (vcard-url (car (bbdb-vcard-values-of-type "URL" "value" t
)))
554 (vcard-notes (bbdb-vcard-values-of-type "NOTE" "value"))
555 (raw-bday (bbdb-vcard-unvcardize-date-time
556 (car (bbdb-vcard-values-of-type "BDAY" "value" t
))))
557 ;; Birthday suitable for storing in BBDB (usable by org-mode):
558 (vcard-bday (when raw-bday
(concat raw-bday
" birthday")))
559 ;; Birthday to search for in BBDB now:
560 (bday-to-search-for vcard-bday
)
561 ;; Non-birthday anniversaries, probably exported by ourselves:
562 (vcard-x-bbdb-anniversaries
563 (bbdb-vcard-split-structured-text
564 (car (bbdb-vcard-values-of-type "X-BBDB-ANNIVERSARY" "value"))
566 (vcard-rev (bbdb-vcard-unvcardize-date-time
567 (car (bbdb-vcard-values-of-type "REV" "value"))))
568 (vcard-categories (bbdb-vcard-values-of-type "CATEGORIES" "value"))
569 ;; The BBDB record to change:
570 (record-freshness-info "BBDB record changed:") ; default user info
573 ;; Try to find an existing one ...
574 ;; (a) try company and net and name:
575 (car (and bbdb-vcard-try-merge
576 (bbdb-vcard-search-intersection
579 org-to-search-for email-to-search-for
)))
580 ;; (b) try company and name:
581 (car (and bbdb-vcard-try-merge
582 (bbdb-vcard-search-intersection
583 (bbdb-records) name-to-search-for org-to-search-for
)))
584 ;; (c) try net and name; we may change company here:
585 (car (and bbdb-vcard-try-merge
586 (bbdb-vcard-search-intersection
588 name-to-search-for nil email-to-search-for
)))
589 ;; (d) try birthday and name; we may change company here:
590 (car (and bbdb-vcard-try-merge
591 (bbdb-vcard-search-intersection
593 name-to-search-for nil nil bday-to-search-for
)))
594 ;; (e) try phone and name; we may change company here:
595 (car (and bbdb-vcard-try-merge
596 (bbdb-vcard-search-intersection
598 name-to-search-for nil nil nil tel-to-search-for
)))
599 ;; No existing record found; make a fresh one:
600 (let ((fresh-record (make-vector bbdb-record-length nil
)))
601 (bbdb-record-set-cache fresh-record
602 (make-vector bbdb-cache-length nil
))
603 (if vcard-rev
; For fresh records,
604 (bbdb-record-putprop ; set creation-date from vcard-rev
605 fresh-record
'creation-date vcard-rev
)
606 (bbdb-invoke-hook 'bbdb-create-hook fresh-record
))
607 (setq record-freshness-info
"BBDB record added:") ; user info
609 (bbdb-akas (bbdb-record-aka bbdb-record
))
610 (bbdb-addresses (bbdb-record-addresses bbdb-record
))
611 (bbdb-phones (bbdb-record-phones bbdb-record
))
612 (bbdb-nets (bbdb-record-net bbdb-record
))
613 (bbdb-raw-notes (bbdb-record-raw-notes bbdb-record
))
616 (bbdb-vcard-elements-of-type "BEGIN") ; get rid of delimiter
617 (bbdb-vcard-elements-of-type "END") ; get rid of delimiter
618 (bbdb-vcard-elements-of-type "VERSION") ; get rid of this too
619 (when name
; which should be the case as N is mandatory in vCard
620 (bbdb-record-set-firstname bbdb-record
(car name
))
621 (bbdb-record-set-lastname bbdb-record
(cadr name
)))
624 (remove (concat (bbdb-record-firstname bbdb-record
)
625 " " (bbdb-record-lastname bbdb-record
))
626 (reduce (lambda (x y
) (union x y
:test
'string
=))
627 (list vcard-nicknames
629 vcard-formatted-names
631 (when vcard-org
(bbdb-record-set-company bbdb-record vcard-org
))
633 bbdb-record
(union vcard-email bbdb-nets
:test
'string
=))
634 (bbdb-record-set-addresses
635 bbdb-record
(union vcard-adrs bbdb-addresses
:test
'equal
))
636 (bbdb-record-set-phones bbdb-record
637 (union vcard-tels bbdb-phones
:test
'equal
))
638 ;; prepare bbdb's notes:
639 (when vcard-url
(push (cons 'www vcard-url
) bbdb-raw-notes
))
641 ;; Put vCard NOTEs under key 'notes (append if necessary).
642 (unless (assq 'notes bbdb-raw-notes
)
643 (push (cons 'notes
"") bbdb-raw-notes
))
644 (setf (cdr (assq 'notes bbdb-raw-notes
))
645 (bbdb-vcard-merge-strings
646 (cdr (assq 'notes bbdb-raw-notes
))
647 (bbdb-vcard-unescape-strings vcard-notes
)
649 (when (or vcard-bday vcard-x-bbdb-anniversaries
)
650 ;; Put vCard BDAY and vCard X-BBDB-ANNIVERSARY's under key
651 ;; 'anniversary (append if necessary) where org-mode can find
652 ;; it. Org-mode doesn't currently (v6.35) bother with time
653 ;; and time zone, though.
654 (when vcard-bday
(push vcard-bday vcard-x-bbdb-anniversaries
))
655 (unless (assq 'anniversary bbdb-raw-notes
)
656 (push (cons 'anniversary
"") bbdb-raw-notes
))
657 (setf (cdr (assq 'anniversary bbdb-raw-notes
))
658 (bbdb-vcard-merge-strings
659 (cdr (assq 'anniversary bbdb-raw-notes
))
660 (bbdb-vcard-unescape-strings vcard-x-bbdb-anniversaries
)
662 (when vcard-categories
663 ;; Put vCard CATEGORIES under key 'mail-alias (append if necessary).
664 (unless (assq 'mail-alias bbdb-raw-notes
)
665 (push (cons 'mail-alias
"") bbdb-raw-notes
))
666 (setf (cdr (assq 'mail-alias bbdb-raw-notes
))
667 (bbdb-vcard-merge-strings
668 (cdr (assq 'mail-alias bbdb-raw-notes
))
671 (while (setq other-vcard-type
(bbdb-vcard-other-element))
672 (when (string-match "^\\([[:alnum:]-]*\\.\\)?AGENT"
673 (symbol-name (car other-vcard-type
)))
674 ;; Notice other vCards inside the current one.
675 (bbdb-vcard-iterate-vcards
676 'bbdb-vcard-import-vcard
; needed for inner v2.1 vCards:
677 (replace-regexp-in-string "\\\\" "" (cdr other-vcard-type
))))
678 (unless (or (and bbdb-vcard-skip-on-import
679 (string-match bbdb-vcard-skip-on-import
680 (symbol-name (car other-vcard-type
))))
681 (and bbdb-vcard-skip-valueless
682 (zerop (length (cdr other-vcard-type
)))))
683 (push (bbdb-vcard-remove-x-bbdb other-vcard-type
) bbdb-raw-notes
)))
684 (bbdb-record-set-raw-notes
686 (remove-duplicates bbdb-raw-notes
:test
'equal
:from-end t
))
687 (bbdb-change-record bbdb-record t
)
688 ;; Tell the user what we've done.
689 (message "%s %s %s -- %s"
690 record-freshness-info
691 (bbdb-record-firstname bbdb-record
)
692 (bbdb-record-lastname bbdb-record
)
693 (replace-regexp-in-string
694 "\n" "; " (or (bbdb-record-company bbdb-record
) "-"))))))
696 (defun bbdb-vcard-from (record)
697 "Return BBDB RECORD as a vCard."
699 (let* ((name (bbdb-record-name record
))
700 (first-name (bbdb-record-firstname record
))
701 (last-name (bbdb-record-lastname record
))
702 (aka (bbdb-record-aka record
))
703 (company (bbdb-record-company record
))
704 (net (bbdb-record-net record
))
705 (phones (bbdb-record-phones record
))
706 (addresses (bbdb-record-addresses record
))
707 (www (bbdb-get-field record
'www
))
709 (bbdb-vcard-split-structured-text (bbdb-record-notes record
)
711 (raw-anniversaries (bbdb-vcard-split-structured-text
712 (bbdb-get-field record
'anniversary
) "\n" t
))
714 "\\([0-9]\\{4\\}-[01][0-9]-[0-3][0-9][t:0-9]*[-+z:0-9]*\\)\\([[:blank:]]+birthday\\)?\\'")
716 (car (bbdb-vcard-split-structured-text
717 (find-if (lambda (x) (string-match birthday-regexp x
))
721 (remove-if (lambda (x) (string-match birthday-regexp x
))
722 raw-anniversaries
:count
1))
723 (creation-date (bbdb-get-field record
'creation-date
))
724 (mail-aliases (bbdb-record-getprop record
725 bbdb-define-all-aliases-field
))
726 (raw-notes (copy-alist (bbdb-record-raw-notes record
))))
727 (bbdb-vcard-insert-vcard-element "BEGIN" "VCARD")
728 (bbdb-vcard-insert-vcard-element "VERSION" "3.0")
729 (bbdb-vcard-insert-vcard-element "FN" (bbdb-vcard-escape-strings name
))
730 (bbdb-vcard-insert-vcard-element
731 "N" (bbdb-vcard-escape-strings last-name
)
732 ";" (bbdb-vcard-escape-strings first-name
)
733 ";;;") ; Additional Names, Honorific Prefixes, Honorific Suffixes
734 (bbdb-vcard-insert-vcard-element
735 "NICKNAME" (bbdb-join (bbdb-vcard-escape-strings aka
) ","))
736 (bbdb-vcard-insert-vcard-element
737 "ORG" (bbdb-vcard-escape-strings company
))
739 (bbdb-vcard-insert-vcard-element
740 "EMAIL;TYPE=INTERNET" (bbdb-vcard-escape-strings mail
)))
741 (dolist (phone phones
)
742 (bbdb-vcard-insert-vcard-element
745 (bbdb-vcard-escape-strings
746 (bbdb-vcard-translate (bbdb-phone-location phone
) t
)))
747 (bbdb-vcard-escape-strings (bbdb-phone-string phone
))))
748 (dolist (address addresses
)
749 (bbdb-vcard-insert-vcard-element
752 (bbdb-vcard-escape-strings
753 (bbdb-vcard-translate (bbdb-address-location address
) t
)))
754 ";;" ; no Postbox, no Extended
755 (bbdb-join (bbdb-vcard-escape-strings (bbdb-address-streets address
))
757 ";" (bbdb-vcard-vcardize-address-element
758 (bbdb-vcard-escape-strings (bbdb-address-city address
)))
759 ";" (bbdb-vcard-vcardize-address-element
760 (bbdb-vcard-escape-strings (bbdb-address-state address
)))
761 ";" (bbdb-vcard-vcardize-address-element
762 (bbdb-vcard-escape-strings (bbdb-address-zip address
)))
763 ";" (bbdb-vcard-vcardize-address-element
764 (bbdb-vcard-escape-strings (bbdb-address-country address
)))))
765 (bbdb-vcard-insert-vcard-element "URL" www
)
767 (bbdb-vcard-insert-vcard-element
768 "NOTE" (bbdb-vcard-escape-strings note
)))
769 (bbdb-vcard-insert-vcard-element "BDAY" birthday
)
770 (bbdb-vcard-insert-vcard-element ; non-birthday anniversaries
771 "X-BBDB-ANNIVERSARY" (bbdb-join other-anniversaries
"\\n"))
772 (bbdb-vcard-insert-vcard-element "REV" creation-date
)
773 (bbdb-vcard-insert-vcard-element
775 (bbdb-join (bbdb-vcard-escape-strings
776 (bbdb-vcard-split-structured-text mail-aliases
"," t
)) ","))
777 ;; prune raw-notes...
778 (dolist (key '(www notes anniversary mail-alias creation-date timestamp
))
779 (setq raw-notes
(assq-delete-all key raw-notes
)))
780 ;; ... and output what's left
781 (dolist (raw-note raw-notes
)
782 (bbdb-vcard-insert-vcard-element
783 (symbol-name (bbdb-vcard-prepend-x-bbdb-maybe (car raw-note
)))
784 (bbdb-vcard-escape-strings (cdr raw-note
))))
785 (bbdb-vcard-insert-vcard-element "END" "VCARD")
786 (bbdb-vcard-insert-vcard-element nil
)) ; newline
791 (defun bbdb-vcard-convert-to-3.0
(vcard)
792 "Convert VCARD from v2.1 to v3.0.
793 Return a version 3.0 vCard as a string. Don't bother about the vCard
794 v3.0 mandatory elements N and FN."
795 ;; Prevent customization of vcard.el's from being changed behind our back:
796 (let ((vcard-standard-filters '(vcard-filter-html)))
798 (bbdb-vcard-insert-vcard-element "BEGIN" "VCARD")
799 (bbdb-vcard-insert-vcard-element "VERSION" "3.0")
800 (dolist (element (remove*
801 "VERSION" (vcard-parse-string vcard
)
802 :key
(lambda (x) (upcase (caar x
))) :test
'string
=))
803 (bbdb-vcard-insert-vcard-element
804 (concat (caar element
)
805 (mapconcat 'bbdb-vcard-parameter-pair
(cdar element
) ""))
806 (bbdb-join (bbdb-vcard-escape-strings (cdr element
)) ";")))
807 (bbdb-vcard-insert-vcard-element "END" "VCARD")
808 (bbdb-vcard-insert-vcard-element nil
)
811 (defun bbdb-vcard-parameter-pair (input)
812 "Return \"parameter=value\" made from INPUT.
813 INPUT is its representation in vcard.el. Return empty string if INPUT
815 (cond ((consp input
) (concat ";" (car input
) "=" (cdr input
)))
816 ((stringp input
) (concat ";TYPE=" input
))
821 (defun bbdb-vcard-values-of-type
822 (type parameter
&optional one-is-enough-p split-value-at-semi-colon-p
)
823 "Return in a list the values of PARAMETER of vCard element of TYPE.
824 The VCard element is read and deleted from current buffer which is
825 supposed to contain a single vCard. If ONE-IS-ENOUGH-P is non-nil,
826 read and delete only the first element of TYPE. If PARAMETER is
827 \"value\" and SPLIT-VALUE-AT-SEMI-COLON-P is non-nil, split the value
828 at semi-colons into a list."
829 (mapcar (lambda (x) (cdr (assoc parameter x
)))
830 (bbdb-vcard-elements-of-type
831 type one-is-enough-p split-value-at-semi-colon-p
)))
833 (defun bbdb-vcard-elements-of-type
834 (type &optional one-is-enough-p split-value-at-semi-colon-p
)
835 "From current buffer read and delete the vCard elements of TYPE.
836 The current buffer is supposed to contain a single vCard. If
837 ONE-IS-ENOUGH-P is non-nil, read and delete only the first element of
838 TYPE. Return a list of alists, one per element. Each alist has a
839 cell with key \"value\" containing the element's value, and may have
840 other elements of the form \(parameter-name . parameter-value). If
841 SPLIT-VALUE-AT-SEMI-COLON-P is non-nil, split the value at key
842 \"value\" at semi-colons into a list."
843 (goto-char (point-min))
844 (let (values parameters read-enough
)
850 "^\\([[:alnum:]-]*\\.\\)?\\(" type
"\\)\\(;.*\\)?:\\(.*\\)$")
852 (goto-char (match-end 2))
853 (setq parameters nil
)
854 (push (cons "value" (if split-value-at-semi-colon-p
855 (bbdb-vcard-split-structured-text
856 (match-string 4) ";")
859 (while (re-search-forward "\\([^;:=]+\\)=\\([^;:]+\\)"
860 (line-end-position) t
)
861 (let* ((parameter-key (downcase (match-string 1)))
862 (parameter-value (downcase (match-string 2)))
863 (parameter-sibling (assoc parameter-key parameters
)))
864 (if parameter-sibling
; i.e., pair with equal key
865 ;; collect vCard parameter list `;a=x;a=y;a=z'
866 ;; into vCard value list `;a=x,y,z'; becoming ("a" . "x,y,z")
867 (setf (cdr parameter-sibling
)
868 (concat (cdr parameter-sibling
) "," parameter-value
))
869 ;; vCard parameter pair `;key=value;' with new key
870 (push (cons parameter-key parameter-value
) parameters
))))
871 (push parameters values
)
872 (delete-region (line-end-position 0) (line-end-position))
873 (when one-is-enough-p
(setq read-enough t
)))
876 (defun bbdb-vcard-other-element ()
877 "From current buffer read and delete the topmost vCard element.
878 Buffer is supposed to contain a single vCard. Return (TYPE . VALUE)."
879 (goto-char (point-min))
880 (when (re-search-forward "^\\([[:graph:]]*?\\):\\(.*\\)$" nil t
)
881 (let ((type (match-string 1))
882 (value (match-string 2)))
883 (delete-region (match-beginning 0) (match-end 0))
884 (cons (intern (downcase type
)) (bbdb-vcard-unescape-strings value
)))))
886 (defun bbdb-vcard-insert-vcard-element (type &rest values
)
887 "Insert a vCard element comprising TYPE, `:', VALUES into current buffer.
888 Take care of TYPE canonicalization, line folding, and closing newline.
889 Do nothing if TYPE is non-nil and VALUES are empty. Insert just a
890 newline if TYPE is nil."
892 (let ((value (bbdb-join values
"")))
893 (unless (zerop (length value
))
894 (insert (bbdb-vcard-fold-line
895 (concat (bbdb-vcard-canonicalize-vcard-type type
)
897 (insert (bbdb-vcard-fold-line ""))))
901 (defun bbdb-vcard-unfold-lines (vcards)
902 "Return folded vCard lines from VCARDS unfolded."
903 (replace-regexp-in-string "\n\\( \\|\t\\)" "" vcards
))
905 (defun bbdb-vcard-fold-line (long-line)
906 "Insert after every 75th position in LONG-LINE a newline and a space."
907 (with-temp-buffer (insert long-line
)
908 (goto-char (point-min))
909 (while (< (goto-char (+ (point) 75))
915 (defun bbdb-vcard-unescape-strings (escaped-strings)
916 "Unescape escaped `;', `,', `\\', and newlines in ESCAPED-STRINGS.
917 ESCAPED-STRINGS may be a string or a sequence of strings."
918 (flet ((unescape (x) (replace-regexp-in-string
919 "\\([\\\\]\\)\\([,;\\]\\)" ""
920 (replace-regexp-in-string "\\\\n" "\n" x
)
922 (bbdb-vcard-process-strings 'unescape escaped-strings
)))
924 (defun bbdb-vcard-escape-strings (unescaped-strings )
925 "Escape `;', `,', `\\', and newlines in UNESCAPED-STRINGS.
926 UNESCAPED-STRINGS may be a string or a sequence of strings."
927 (flet ((escape (x) (replace-regexp-in-string ; from 2.1 conversion:
928 "\r" "" (replace-regexp-in-string
929 "\n" "\\\\n" (replace-regexp-in-string
930 "\\(\\)[,;\\]" "\\\\" (or x
"")
932 (bbdb-vcard-process-strings 'escape unescaped-strings
)))
934 (defun bbdb-vcard-process-strings (string-processor strings
)
935 "Apply STRING-PROCESSOR to STRINGS.
936 STRINGS may be a string or a sequence of strings."
937 (if (stringp strings
)
938 (funcall string-processor strings
)
939 (mapcar string-processor strings
)))
943 (defun bbdb-vcard-remove-x-bbdb (vcard-element)
944 "Remove the `X-BBDB-' prefix from the type part of VCARD-ELEMENT if any."
945 (cons (intern (replace-regexp-in-string
946 "^X-BBDB-" "" (symbol-name (car vcard-element
))))
947 (cdr vcard-element
)))
949 (defun bbdb-vcard-prepend-x-bbdb-maybe (bbdb-fieldname)
950 "If BBDB-FIELDNAME is in `bbdb-vcard-x-bbdb-candidates', prepend `X-BBDB'."
951 (if (member bbdb-fieldname bbdb-vcard-x-bbdb-candidates
)
952 (intern (concat "x-bbdb-" (symbol-name bbdb-fieldname
)))
953 bbdb-fieldname
)) ; lowercase more consistent here
955 (defun bbdb-vcard-unvcardize-name (vcard-name)
956 "Convert VCARD-NAME (type N) into (FIRSTNAME LASTNAME)."
957 (if (stringp vcard-name
) ; unstructured N
958 (bbdb-divide-name vcard-name
)
961 (bbdb-join (bbdb-vcard-split-structured-text x
"," t
)
963 vcard-name
))) ; flatten comma-separated substructure
964 (list (concat (nth 3 vcard-name
) ; honorific prefixes
965 (unless (zerop (length (nth 3 vcard-name
))) " ")
966 (nth 1 vcard-name
) ; given name
967 (unless (zerop (length (nth 2 vcard-name
))) " ")
968 (nth 2 vcard-name
)) ; additional names
969 (concat (nth 0 vcard-name
) ; family name
970 (unless (zerop (length (nth 4 vcard-name
))) " ")
971 (nth 4 vcard-name
)))))) ; honorific suffixes
973 (defun bbdb-vcard-unvcardize-org (vcard-org)
974 "Convert VCARD-ORG (type ORG), which may be a list, into a string."
975 (if (or (null vcard-org
)
976 (stringp vcard-org
)) ; unstructured, probably non-standard ORG
977 vcard-org
; Company, unit 1, unit 2...
978 (bbdb-join vcard-org
"\n")))
980 (defun bbdb-vcard-unvcardize-adr (vcard-adr)
981 "Convert VCARD-ADR into BBDB format.
982 Turn a vCard element of type ADR into (TYPE STREETS CITY STATE ZIP
984 (let ((adr-type (or (cdr (assoc "type" vcard-adr
)) ""))
985 (streets ; all comma-separated sub-elements of
986 (remove ; Postbox, Extended, Streets go into one list
989 (bbdb-vcard-split-structured-text x
"," t
))
990 (subseq (cdr (assoc "value" vcard-adr
))
992 (non-streets ; turn comma-separated substructure into
993 (mapcar ; newline-separated text
994 (lambda (x) (bbdb-join
995 (bbdb-vcard-split-structured-text x
"," t
)
997 (subseq (cdr (assoc "value" vcard-adr
))
999 (vector (bbdb-vcard-translate adr-type
)
1001 (or (elt non-streets
0) "") ; City
1002 (or (elt non-streets
1) "") ; State
1003 (or (elt non-streets
2) "") ; Zip
1004 (or (elt non-streets
3) "")))) ; Country
1006 (defun bbdb-vcard-unvcardize-date-time (date-time)
1007 "If necessary, make DATE-TIME usable for storage in BBDB.
1008 Convert yyyymmdd, yyyymmddThhmmss, or yyymmddThhmmssZhhmm into
1009 yyyy-mm-dd, yyyy-mm-ddThh:mm:ss, or yyy-mm-ddThh:mm:ssZhh:mm
1010 respectively. Discard fractions of a second. Return anything else
1012 (if (and (stringp date-time
)
1014 "\\([0-9]\\{4\\}\\)-?\\([0-2][0-9]\\)-?\\([0-3][0-9]\\)\\(?:t\\([0-5][0-9]\\):?\\([0-5][0-9]\\):?\\([0-5][0-9]\\)\\(?:[,.0-9]*\\(\\([+-][0-5][0-9]\\):?\\([0-5][0-9]\\)?\\|z\\)\\)?\\)?"
1017 (match-string 1 date-time
) "-"
1018 (match-string 2 date-time
) "-" (match-string 3 date-time
)
1019 (when (match-string 6 date-time
) ; seconds part of time
1021 "T" (match-string 4 date-time
) ":"
1022 (match-string 5 date-time
) ":" (match-string 6 date-time
)
1023 (when (match-string 7 date-time
) ; time zone
1024 (if (match-string 9 date-time
) ; time zone minute
1025 (concat (match-string 8 date-time
) ; time zone hour
1026 ":" (match-string 9 date-time
)) ; time zone minute
1030 (defun bbdb-vcard-vcardize-address-element (address-element)
1031 "Replace escaped newlines in ADDRESS-ELEMENT by commas."
1032 (replace-regexp-in-string "\\\\n" "," address-element
))
1034 (defun bbdb-vcard-translate (label &optional exportp
)
1035 "Translate LABEL from vCard to BBDB or, if EXPORTP is non-nil, vice versa.
1036 Translations are defined in `bbdb-vcard-import-translation-table' and
1037 `bbdb-vcard-export-translation-table' respectively."
1040 (or (assoc-default label
1042 bbdb-vcard-export-translation-table
1043 bbdb-vcard-import-translation-table
) 'string-match
)
1046 (defun bbdb-vcard-merge-strings (old-string new-strings separator
)
1047 "Merge strings successively from list NEW-STRINGS into OLD-STRING.
1048 If an element of NEW-STRINGS is already in OLD-STRING, leave
1049 OLD-STRING unchanged. Otherwise append SEPARATOR and NEW-STRING."
1052 (dolist (new-string new-strings
)
1053 (unless (prog1 (search-backward new-string nil t
)
1054 (goto-char (point-max)))
1055 (unless (zerop (buffer-size)) (insert separator
))
1056 (insert new-string
)))
1059 (defun bbdb-vcard-split-structured-text
1060 (text separator
&optional return-always-list-p
)
1061 "Split TEXT at unescaped occurrences of SEPARATOR; return parts in a list.
1062 Return text unchanged if there aren't any separators and RETURN-ALWAYS-LIST-P
1064 (when (stringp text
)
1065 (let ((string-elements
1067 (replace-regexp-in-string
1068 (concat "\\\\\r" separator
) (concat "\\\\" separator
)
1069 (replace-regexp-in-string separator
(concat "\r" separator
) text
))
1070 (concat "\r" separator
))))
1071 (if (and (null return-always-list-p
)
1072 (= 1 (length string-elements
)))
1073 (car string-elements
)
1076 (defun bbdb-vcard-canonicalize-vcard-type (&rest strings
)
1077 "Concatenate STRINGS and apply `bbdb-vcard-type-canonicalizer' to them."
1078 (funcall bbdb-vcard-type-canonicalizer
(bbdb-join strings
"")))
1080 (defun bbdb-vcard-write-buffer (vcard-file-name)
1081 "Write current buffer to VCARD-FILE-NAME.
1082 Create directories where necessary."
1083 (make-directory (file-name-directory vcard-file-name
) t
)
1084 (let ((buffer-file-coding-system bbdb-vcard-export-coding-system
))
1085 (write-region nil nil vcard-file-name nil nil nil t
)))
1087 (defun bbdb-vcard-make-file-name (bbdb-record &optional used-up-basenames
)
1088 "Come up with a vCard filename given a BBDB-RECORD.
1089 Make it unique against the list USED-UP-BASENAMES."
1090 (let ((name (bbdb-record-name bbdb-record
))
1091 (aka (car (bbdb-record-aka bbdb-record
)))
1097 (replace-regexp-in-string
1099 (or (unless (zerop (length name
)) name
)
1100 (unless (zerop (length aka
)) aka
)
1102 (unless (zerop unique-number
)
1103 (concat "-" (number-to-string unique-number
)))
1106 (incf unique-number
))
1109 (defmacro bbdb-vcard-search-intersection
1110 (records &optional name company net notes phone
)
1111 "Search RECORDS for records that match each non-nil argument."
1114 (if phone
`(when ,phone
(bbdb-search ,records nil nil nil nil
,phone
))
1117 (if notes
`(when ,notes
(bbdb-search ,phone-search nil nil nil
,notes
))
1120 (if net
`(when ,net
(bbdb-search ,notes-search nil nil
,net
))
1123 (if company
`(when ,company
(bbdb-search ,net-search nil
,company
))
1126 (if name
`(when ,name
(bbdb-search ,company-search
,name
))
1132 (provide 'bbdb-vcard
)
1134 ;;; bbdb-vcard.el ends here
1136 ; LocalWords: vcard firstname