New version number: 0.3
[bbdb-vcard.git] / bbdb-vcard.el
blob7cf2bbad652f7d9b8424999fdd127696882abcf5
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
8 ;; Version: 0.3
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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
30 ;;; Commentary:
31 ;;
32 ;; Purpose
33 ;; =======
34 ;;
35 ;; Import and export of vCards as defined in RFC 2425 and RFC 2426
36 ;; to/from The Insidious Big Brother Database (BBDB).
37 ;;
39 ;; Usage
40 ;; =====
42 ;; vCard Import
43 ;; ------------
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.
53 ;; vCard Export
54 ;; ------------
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
61 ;; respectively.
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'.
71 ;; Installation
72 ;; ============
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)
80 ;; Implementation
81 ;; ==============
83 ;; vCard Import
84 ;; ------------
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
90 ;;
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
100 ;; record created.
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
125 ;; the prefix.
127 ;; VCard type X-BBDB-ANNIVERSARY may contain (previously exported)
128 ;; newline-separated non-birthday anniversaries that are meant to be
129 ;; read by org-mode.
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
139 ;; imported as well.
142 ;; Handling of the individual types defined in RFC2426 during import
143 ;; (assuming default label translation and no vCard type exclusion):
144 ;; "
145 ;; |----------------------+----------------------------------------|
146 ;; | VCARD TYPE; | STORAGE IN BBDB |
147 ;; | PARAMETERS | |
148 ;; |----------------------+----------------------------------------|
149 ;; | VERSION | - |
150 ;; |----------------------+----------------------------------------|
151 ;; | N | First occurrence: |
152 ;; | | Firstname |
153 ;; | | Lastname |
154 ;; | | |
155 ;; | | Rest: |
156 ;; | | AKAs (append) |
157 ;; |----------------------+----------------------------------------|
158 ;; | FN | AKAs (append) |
159 ;; | NICKNAME | AKAs (append) |
160 ;; |----------------------+----------------------------------------|
161 ;; | ORG | First occurrence: |
162 ;; | | Company |
163 ;; | | |
164 ;; | | Rest: |
165 ;; | | Notes<org |
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 |
198 ;; | TZ | Notes<tz |
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 ;; |----------------------+----------------------------------------|
215 ;; "
217 ;; vCard Export
218 ;; ------------
220 ;; VCard types N (only fields lastname, firstname) and FN both come
221 ;; from BBDB's Name.
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
246 ;; change.
250 ;;; History:
254 ;;; Code:
256 (require 'cl)
257 (require 'bbdb)
258 (require 'vcard)
259 (require 'bbdb-com)
261 (defconst bbdb-vcard-version "0.3"
262 "Version of the vCard importer/exporter.
263 The major part increases on user-visible changes.")
267 ;;;; User Variables
269 (defgroup bbdb-vcard nil
270 "Customizations for vCards"
271 :group 'bbdb)
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-'."
276 :group 'bbdb-vcard
277 :type 'regexp)
279 (defcustom bbdb-vcard-skip-valueless t
280 "Skip vCard element types with an empty value.
281 Nil means insert empty types into BBDB."
282 :group 'bbdb-vcard
283 :type 'boolean)
285 (defcustom bbdb-vcard-import-translation-table
286 '(("CELL\\|CAR" . "Mobile")
287 ("WORK" . "Office")
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."
295 :group 'bbdb-vcard
296 :type '(alist :key-type
297 (choice regexp (const :tag "Empty (as default)" "^$"))
298 :value-type string))
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."
303 :group 'bbdb-vcard
304 :type 'boolean)
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'."
309 :group 'bbdb-vcard
310 :type 'function)
312 (defcustom bbdb-vcard-x-bbdb-candidates
313 '(attribution
314 finger-host
315 gnus-score
316 mark-char
317 mail-name
318 face
319 tex-name
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
324 is removed again."
325 :group 'bbdb-vcard
326 :type '(repeat symbol))
328 (defcustom bbdb-vcard-export-translation-table
329 '(("Mobile" . "CELL")
330 ("Office" . "WORK"))
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)."
334 :group 'bbdb-vcard
335 :type '(alist :key-type
336 (choice regexp (const :tag "Empty (as default)" "^$"))
337 :value-type string))
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."
342 :group 'bbdb-vcard
343 :type 'symbol)
345 (defcustom bbdb-vcard-default-dir "~/exported-vcards/"
346 "Default storage directory for exported vCards.
347 Nil means current directory."
348 :group 'bbdb-vcard
349 :type '(choice directory (const :tag "Current directory" nil)))
353 ;;;; User Functions
355 ;;;###autoload
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."
359 (interactive "r")
360 (bbdb-vcard-iterate-vcards 'bbdb-vcard-import-vcard
361 (buffer-substring-no-properties begin end)))
363 ;;;###autoload
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)))
371 ;;;###autoload
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))
378 (with-temp-buffer
379 (insert-file-contents vcard-file)
380 (bbdb-vcard-import-region (point-min) (point-max)))))
382 ;;;###autoload
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).
386 \\<bbdb-mode-map>\
387 If \"\\[bbdb-apply-next-command-to-all-records]\\[bbdb-vcard-export]\"\
388 is used instead of simply \"\\[bbdb-vcard-export]\", then export all \
389 records currently
390 in the *BBDB* buffer. If used with prefix argument, store records
391 in individual files."
392 (interactive
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)))
396 (list
397 (if all-records-p
398 (if current-prefix-arg
399 (read-directory-name "Write vCard files to directory: "
400 bbdb-vcard-default-dir nil 42)
401 (read-file-name
402 "Write vCards to file: "
403 bbdb-vcard-default-dir
404 nil nil
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
410 (if all-records-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
415 (progn
416 (dolist (record records)
417 (with-temp-buffer
418 (let ((basename
419 (bbdb-vcard-make-file-name record
420 used-up-basenames)))
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
432 (with-temp-buffer
433 (insert vcard)
434 (bbdb-vcard-write-buffer filename-or-directory)))))
436 ;;;###autoload
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.
439 \\<bbdb-mode-map>\
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
444 the *BBDB* buffer."
445 (interactive (let ((all-records-p (bbdb-do-all-records-p)))
446 (list all-records-p)))
447 (if all-records-p
448 (let ((records (progn (set-buffer bbdb-buffer-name)
449 (mapcar 'car bbdb-records))))
450 (kill-new "")
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."
467 (with-temp-buffer
468 (insert vcards)
469 (goto-char (point-min))
470 ;; Change CRLF into CR if necessary, dealing with inconsistent line
471 ;; endings.
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\\)"
478 nil t)
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."
488 (with-temp-buffer
489 (insert 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."
495 (with-temp-buffer
496 (insert vcard)
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:
502 (name-to-search-for
503 (when raw-name (if (stringp raw-name)
504 raw-name
505 (concat (nth 1 raw-name) ;given name
506 " .*"
507 (nth 0 raw-name))))) ; family name
508 ;; Additional names from prefixed types like A.N, B.N, etc.:
509 (other-names
510 (mapcar
511 (lambda (n)
512 (bbdb-join (bbdb-vcard-unvcardize-name (cdr (assoc "value" n)))
513 " "))
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")))
517 (vcard-nicknames
518 (bbdb-vcard-unescape-strings
519 (bbdb-vcard-split-structured-text
520 (car (bbdb-vcard-values-of-type "NICKNAME" "value"))
521 "," t)))
522 ;; Company suitable for storing in BBDB:
523 (vcard-org
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:
532 (email-to-search-for
533 (when vcard-email
534 (concat "\\(" (bbdb-join vcard-email "\\)\\|\\(") "\\)")))
535 ;; Phone numbers suitable for storing in BBDB:
536 (vcard-tels
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:
543 (tel-to-search-for
544 (when vcard-tels
545 (concat "\\("
546 (mapconcat (lambda (x) (elt x 1))
547 vcard-tels "\\)\\|\\(")
548 "\\)")))
549 ;; Addresses
550 (vcard-adrs
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"))
565 "\\\\n" t))
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
571 (bbdb-record
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
577 (bbdb-records)
578 name-to-search-for
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
587 (bbdb-records)
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
592 (bbdb-records)
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
597 (bbdb-records)
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
608 fresh-record)))
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))
614 notes
615 other-vcard-type)
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)))
622 (bbdb-record-set-aka
623 bbdb-record
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
628 other-names
629 vcard-formatted-names
630 bbdb-akas))))
631 (when vcard-org (bbdb-record-set-company bbdb-record vcard-org))
632 (bbdb-record-set-net
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))
640 (when vcard-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)
648 ";\n")))
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)
661 "\n")))
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))
669 vcard-categories
670 ",")))
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
685 bbdb-record
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."
698 (with-temp-buffer
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))
708 (notes
709 (bbdb-vcard-split-structured-text (bbdb-record-notes record)
710 ";\n" t))
711 (raw-anniversaries (bbdb-vcard-split-structured-text
712 (bbdb-get-field record 'anniversary) "\n" t))
713 (birthday-regexp
714 "\\([0-9]\\{4\\}-[01][0-9]-[0-3][0-9][t:0-9]*[-+z:0-9]*\\)\\([[:blank:]]+birthday\\)?\\'")
715 (birthday
716 (car (bbdb-vcard-split-structured-text
717 (find-if (lambda (x) (string-match birthday-regexp x))
718 raw-anniversaries)
719 " " t)))
720 (other-anniversaries
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))
738 (dolist (mail net)
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
743 (concat
744 "TEL;TYPE="
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
750 (concat
751 "ADR;TYPE="
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))
756 ",")
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)
766 (dolist (note notes)
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
774 "CATEGORIES"
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
787 (buffer-string)))
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)))
797 (with-temp-buffer
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)
809 (buffer-string))))
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
814 is nil."
815 (cond ((consp input) (concat ";" (car input) "=" (cdr input)))
816 ((stringp input) (concat ";TYPE=" input))
817 ((null 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)
845 (while
846 (and
847 (not read-enough)
848 (re-search-forward
849 (concat
850 "^\\([[:alnum:]-]*\\.\\)?\\(" type "\\)\\(;.*\\)?:\\(.*\\)$")
851 nil t))
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) ";")
857 (match-string 4)))
858 parameters)
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)))
874 (nreverse values)))
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."
891 (if type
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)
896 ":" value)))))
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))
910 (point-max))
911 (insert "\n "))
912 (insert "\n")
913 (buffer-string)))
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)
921 nil nil 1)))
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 "")
931 nil nil 1)))))
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)
959 (let ((vcard-name
960 (mapcar (lambda (x)
961 (bbdb-join (bbdb-vcard-split-structured-text x "," t)
962 " "))
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
983 COUNTRY)."
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
987 "" (reduce 'append
988 (mapcar (lambda (x)
989 (bbdb-vcard-split-structured-text x "," t))
990 (subseq (cdr (assoc "value" vcard-adr))
991 0 3)))))
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)
996 "\n"))
997 (subseq (cdr (assoc "value" vcard-adr))
998 3 nil))))
999 (vector (bbdb-vcard-translate adr-type)
1000 streets
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
1011 unchanged."
1012 (if (and (stringp date-time)
1013 (string-match
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\\)\\)?\\)?"
1015 date-time))
1016 (concat
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
1020 (concat
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
1027 "Z")))))
1028 date-time))
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."
1038 (when label
1039 (capitalize
1040 (or (assoc-default label
1041 (if exportp
1042 bbdb-vcard-export-translation-table
1043 bbdb-vcard-import-translation-table) 'string-match)
1044 label))))
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."
1050 (with-temp-buffer
1051 (insert old-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)))
1057 (buffer-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
1063 is nil."
1064 (when (stringp text)
1065 (let ((string-elements
1066 (split-string
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)
1074 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)))
1092 (unique-number 0)
1093 filename)
1094 (while (member
1095 (setq filename
1096 (concat
1097 (replace-regexp-in-string
1098 "[[:blank:]]+" "_"
1099 (or (unless (zerop (length name)) name)
1100 (unless (zerop (length aka)) aka)
1101 "bbdb-record"))
1102 (unless (zerop unique-number)
1103 (concat "-" (number-to-string unique-number)))
1104 ".vcf"))
1105 used-up-basenames)
1106 (incf unique-number))
1107 filename))
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."
1112 (let*
1113 ((phone-search
1114 (if phone `(when ,phone (bbdb-search ,records nil nil nil nil ,phone))
1115 records))
1116 (notes-search
1117 (if notes `(when ,notes (bbdb-search ,phone-search nil nil nil ,notes))
1118 phone-search))
1119 (net-search
1120 (if net `(when ,net (bbdb-search ,notes-search nil nil ,net))
1121 notes-search))
1122 (company-search
1123 (if company `(when ,company (bbdb-search ,net-search nil ,company))
1124 net-search))
1125 (name-search
1126 (if name `(when ,name (bbdb-search ,company-search ,name))
1127 company-search)))
1128 name-search))
1132 (provide 'bbdb-vcard)
1134 ;;; bbdb-vcard.el ends here
1136 ; LocalWords: vcard firstname