mumboe-vpim 0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. data/CHANGES +510 -0
  2. data/COPYING +58 -0
  3. data/README +185 -0
  4. data/lib/vpim/address.rb +219 -0
  5. data/lib/vpim/agent/atomize.rb +104 -0
  6. data/lib/vpim/agent/base.rb +73 -0
  7. data/lib/vpim/agent/calendars.rb +173 -0
  8. data/lib/vpim/agent/handler.rb +26 -0
  9. data/lib/vpim/agent/ics.rb +161 -0
  10. data/lib/vpim/attachment.rb +102 -0
  11. data/lib/vpim/date.rb +222 -0
  12. data/lib/vpim/dirinfo.rb +277 -0
  13. data/lib/vpim/duration.rb +119 -0
  14. data/lib/vpim/enumerator.rb +32 -0
  15. data/lib/vpim/field.rb +614 -0
  16. data/lib/vpim/icalendar.rb +384 -0
  17. data/lib/vpim/maker/vcard.rb +16 -0
  18. data/lib/vpim/property/base.rb +193 -0
  19. data/lib/vpim/property/common.rb +315 -0
  20. data/lib/vpim/property/location.rb +38 -0
  21. data/lib/vpim/property/priority.rb +43 -0
  22. data/lib/vpim/property/recurrence.rb +69 -0
  23. data/lib/vpim/property/resources.rb +24 -0
  24. data/lib/vpim/repo.rb +261 -0
  25. data/lib/vpim/rfc2425.rb +367 -0
  26. data/lib/vpim/rrule.rb +591 -0
  27. data/lib/vpim/time.rb +40 -0
  28. data/lib/vpim/vcard.rb +1456 -0
  29. data/lib/vpim/version.rb +18 -0
  30. data/lib/vpim/vevent.rb +187 -0
  31. data/lib/vpim/view.rb +90 -0
  32. data/lib/vpim/vjournal.rb +58 -0
  33. data/lib/vpim/vpim.rb +65 -0
  34. data/lib/vpim/vtodo.rb +103 -0
  35. data/lib/vpim.rb +13 -0
  36. data/samples/README.mutt +93 -0
  37. data/samples/ab-query.rb +57 -0
  38. data/samples/agent.ru +10 -0
  39. data/samples/cmd-itip.rb +156 -0
  40. data/samples/ex_cpvcard.rb +55 -0
  41. data/samples/ex_get_vcard_photo.rb +22 -0
  42. data/samples/ex_mkv21vcard.rb +34 -0
  43. data/samples/ex_mkvcard.rb +64 -0
  44. data/samples/ex_mkyourown.rb +29 -0
  45. data/samples/ics-dump.rb +210 -0
  46. data/samples/ics-to-rss.rb +84 -0
  47. data/samples/mutt-aliases-to-vcf.rb +45 -0
  48. data/samples/osx-wrappers.rb +86 -0
  49. data/samples/reminder.rb +209 -0
  50. data/samples/rrule.rb +71 -0
  51. data/samples/tabbed-file-to-vcf.rb +390 -0
  52. data/samples/vcf-dump.rb +86 -0
  53. data/samples/vcf-lines.rb +61 -0
  54. data/samples/vcf-to-ics.rb +22 -0
  55. data/samples/vcf-to-mutt.rb +121 -0
  56. data/test/test_agent_atomize.rb +84 -0
  57. data/test/test_agent_calendars.rb +128 -0
  58. data/test/test_agent_ics.rb +96 -0
  59. data/test/test_all.rb +17 -0
  60. data/test/test_date.rb +120 -0
  61. data/test/test_dur.rb +41 -0
  62. data/test/test_field.rb +156 -0
  63. data/test/test_ical.rb +437 -0
  64. data/test/test_misc.rb +13 -0
  65. data/test/test_repo.rb +129 -0
  66. data/test/test_rrule.rb +1030 -0
  67. data/test/test_vcard.rb +973 -0
  68. data/test/test_view.rb +79 -0
  69. metadata +140 -0
data/lib/vpim/vcard.rb ADDED
@@ -0,0 +1,1456 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'vpim/vpim'
10
+ require 'vpim/attachment'
11
+ require 'vpim/dirinfo'
12
+
13
+ require 'open-uri'
14
+ require 'stringio'
15
+
16
+ module Vpim
17
+ # A vCard, a specialization of a directory info object.
18
+ #
19
+ # The vCard format is specified by:
20
+ # - RFC2426[http://www.ietf.org/rfc/rfc2426.txt]: vCard MIME Directory Profile (vCard 3.0)
21
+ # - RFC2425[http://www.ietf.org/rfc/rfc2425.txt]: A MIME Content-Type for Directory Information
22
+ #
23
+ # This implements vCard 3.0, but it is also capable of working with vCard 2.1
24
+ # if used with care.
25
+ #
26
+ # All line values can be accessed with Vcard#value, Vcard#values, or even by
27
+ # iterating through Vcard#lines. Line types that don't have specific support
28
+ # and non-standard line types ("X-MY-SPECIAL", for example) will be returned
29
+ # as a String, with any base64 or quoted-printable encoding removed.
30
+ #
31
+ # Specific support exists to return more useful values for the standard vCard
32
+ # types, where appropriate.
33
+ #
34
+ # The wrapper functions (#birthday, #nicknames, #emails, etc.) exist
35
+ # partially as an API convenience, and partially as a place to document
36
+ # the values returned for the more complex types, like PHOTO and EMAIL.
37
+ #
38
+ # For types that do not sensibly occur multiple times (like BDAY or GEO),
39
+ # sometimes a wrapper exists only to return a single line, using #value.
40
+ # However, if you find the need, you can still call #values to get all the
41
+ # lines, and both the singular and plural forms will eventually be
42
+ # implemented.
43
+ #
44
+ # For more information see:
45
+ # - RFC2426[http://www.ietf.org/rfc/rfc2426.txt]: vCard MIME Directory Profile (vCard 3.0)
46
+ # - RFC2425[http://www.ietf.org/rfc/rfc2425.txt]: A MIME Content-Type for Directory Information
47
+ # - vCard2.1[http://www.imc.org/pdi/pdiproddev.html]: vCard 2.1 Specifications
48
+ #
49
+ # vCards are usually transmitted in files with <code>.vcf</code>
50
+ # extensions.
51
+ #
52
+ # = Examples
53
+ #
54
+ # - link:ex_mkvcard.txt: example of creating a vCard
55
+ # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
56
+ # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
57
+ # - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards
58
+ # - link:ex_get_vcard_photo.txt: pull photo data from a vCard
59
+ # - link:ab-query.txt: query the OS X Address Book to find vCards
60
+ # - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful
61
+ # with Mutt (see link:README.mutt for details)
62
+ # - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a
63
+ # (small but) complete application contributed by Dane G. Avilla, thanks!
64
+ # - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards
65
+ # - link:vcf-dump.txt: utility for dumping contents of .vcf files
66
+ class Vcard < DirectoryInfo
67
+
68
+ # Represents the value of an ADR field.
69
+ #
70
+ # #location, #preferred, and #delivery indicate information about how the
71
+ # address is to be used, the other attributes are parts of the address.
72
+ #
73
+ # Using values other than those defined for #location or #delivery is
74
+ # unlikely to be portable, or even conformant.
75
+ #
76
+ # All attributes are optional. #location and #delivery can be set to arrays
77
+ # of strings.
78
+ class Address
79
+ # post office box (String)
80
+ attr_accessor :pobox
81
+ # seldom used, its not clear what it is for (String)
82
+ attr_accessor :extended
83
+ # street address (String)
84
+ attr_accessor :street
85
+ # usually the city (String)
86
+ attr_accessor :locality
87
+ # usually the province or state (String)
88
+ attr_accessor :region
89
+ # postal code (String)
90
+ attr_accessor :postalcode
91
+ # country name (String)
92
+ attr_accessor :country
93
+ # home, work (Array of String): the location referred to by the address
94
+ attr_accessor :location
95
+ # true, false (boolean): where this is the preferred address (for this location)
96
+ attr_accessor :preferred
97
+ # postal, parcel, dom (domestic), intl (international) (Array of String): delivery
98
+ # type of this address
99
+ attr_accessor :delivery
100
+
101
+ # nonstandard types, their meaning is undefined (Array of String). These
102
+ # might be found during decoding, but shouldn't be set during encoding.
103
+ attr_reader :nonstandard
104
+
105
+ # Used to simplify some long and tedious code. These symbols are in the
106
+ # order required for the ADR field structured TEXT value, the order
107
+ # cannot be changed.
108
+ @@adr_parts = [
109
+ :@pobox,
110
+ :@extended,
111
+ :@street,
112
+ :@locality,
113
+ :@region,
114
+ :@postalcode,
115
+ :@country,
116
+ ]
117
+
118
+ # TODO
119
+ # - #location?
120
+ # - #delivery?
121
+ def initialize #:nodoc:
122
+ # TODO - Add #label to support LABEL. Try to find LABEL
123
+ # in either same group, or with sam params.
124
+ @@adr_parts.each do |part|
125
+ instance_variable_set(part, '')
126
+ end
127
+
128
+ @location = []
129
+ @preferred = false
130
+ @delivery = []
131
+ @nonstandard = []
132
+ end
133
+
134
+ def encode #:nodoc:
135
+ parts = @@adr_parts.map do |part|
136
+ instance_variable_get(part)
137
+ end
138
+
139
+ value = Vpim.encode_text_list(parts, ";")
140
+
141
+ params = [ @location, @delivery, @nonstandard ]
142
+ params << 'pref' if @preferred
143
+ params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
144
+
145
+ paramshash = {}
146
+
147
+ paramshash['TYPE'] = params if params.first
148
+
149
+ Vpim::DirectoryInfo::Field.create( 'ADR', value, paramshash)
150
+ end
151
+
152
+ def Address.decode(card, field) #:nodoc:
153
+ adr = new
154
+
155
+ parts = Vpim.decode_text_list(field.value_raw, ';')
156
+
157
+ @@adr_parts.each_with_index do |part,i|
158
+ adr.instance_variable_set(part, parts[i] || '')
159
+ end
160
+
161
+ params = field.pvalues('TYPE')
162
+
163
+ if params
164
+ params.each do |p|
165
+ p.downcase!
166
+ case p
167
+ when 'home', 'work'
168
+ adr.location << p
169
+ when 'postal', 'parcel', 'dom', 'intl'
170
+ adr.delivery << p
171
+ when 'pref'
172
+ adr.preferred = true
173
+ else
174
+ adr.nonstandard << p
175
+ end
176
+ end
177
+ # Strip duplicates
178
+ [ adr.location, adr.delivery, adr.nonstandard ].each do |a|
179
+ a.uniq!
180
+ end
181
+ end
182
+
183
+ adr
184
+ end
185
+ end
186
+
187
+ # Represents the value of an EMAIL field.
188
+ class Email < String
189
+ # true, false (boolean): whether this is the preferred email address
190
+ attr_accessor :preferred
191
+ # internet, x400 (String): the email address format, rarely specified
192
+ # since the default is 'internet'
193
+ attr_accessor :format
194
+ # home, work (Array of String): the location referred to by the address. The
195
+ # inclusion of location parameters in a vCard seems to be non-conformant,
196
+ # strictly speaking, but also seems to be widespread.
197
+ attr_accessor :location
198
+ # nonstandard types, their meaning is undefined (Array of String). These
199
+ # might be found during decoding, but shouldn't be set during encoding.
200
+ attr_reader :nonstandard
201
+
202
+ def initialize(email='') #:nodoc:
203
+ @preferred = false
204
+ @format = 'internet'
205
+ @location = []
206
+ @nonstandard = []
207
+ super(email)
208
+ end
209
+
210
+ def inspect #:nodoc:
211
+ s = "#<#{self.class.to_s}: #{to_str.inspect}"
212
+ s << ", pref" if preferred
213
+ s << ", #{format}" if format != 'internet'
214
+ s << ", " << @location.join(", ") if @location.first
215
+ s << ", #{@nonstandard.join(", ")}" if @nonstandard.first
216
+ s
217
+ end
218
+
219
+ def encode #:nodoc:
220
+ value = to_str.strip
221
+
222
+ if value.length < 1
223
+ raise InvalidEncodingError, "EMAIL must have a value"
224
+ end
225
+
226
+ params = [ @location, @nonstandard ]
227
+ params << @format if @format != 'internet'
228
+ params << 'pref' if @preferred
229
+
230
+ params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
231
+
232
+ paramshash = {}
233
+
234
+ paramshash['TYPE'] = params if params.first
235
+
236
+ Vpim::DirectoryInfo::Field.create( 'EMAIL', value, paramshash)
237
+ end
238
+
239
+ def Email.decode(field) #:nodoc:
240
+ value = field.to_text.strip
241
+
242
+ if value.length < 1
243
+ raise InvalidEncodingError, "EMAIL must have a value"
244
+ end
245
+
246
+ eml = Email.new(value)
247
+
248
+ params = field.pvalues('TYPE')
249
+
250
+ if params
251
+ params.each do |p|
252
+ p.downcase!
253
+ case p
254
+ when 'home', 'work'
255
+ eml.location << p
256
+ when 'pref'
257
+ eml.preferred = true
258
+ when 'x400', 'internet'
259
+ eml.format = p
260
+ else
261
+ eml.nonstandard << p
262
+ end
263
+ end
264
+ # Strip duplicates
265
+ [ eml.location, eml.nonstandard ].each do |a|
266
+ a.uniq!
267
+ end
268
+ end
269
+
270
+ eml
271
+ end
272
+ end
273
+
274
+ # Represents the value of a TEL field.
275
+ #
276
+ # The value is supposed to be a "X.500 Telephone Number" according to RFC
277
+ # 2426, but that standard is not freely available. Otherwise, anything that
278
+ # looks like a phone number should be OK.
279
+ class Telephone < String
280
+ # true, false (boolean): whether this is the preferred email address
281
+ attr_accessor :preferred
282
+ # home, work, cell, car, pager (Array of String): the location
283
+ # of the device
284
+ attr_accessor :location
285
+ # voice, fax, video, msg, bbs, modem, isdn, pcs (Array of String): the
286
+ # capabilities of the device
287
+ attr_accessor :capability
288
+ # nonstandard types, their meaning is undefined (Array of String). These
289
+ # might be found during decoding, but shouldn't be set during encoding.
290
+ attr_reader :nonstandard
291
+
292
+ def initialize(telephone='') #:nodoc:
293
+ @preferred = false
294
+ @location = []
295
+ @capability = []
296
+ @nonstandard = []
297
+ super(telephone)
298
+ end
299
+
300
+ def inspect #:nodoc:
301
+ s = "#<#{self.class.to_s}: #{to_str.inspect}"
302
+ s << ", pref" if preferred
303
+ s << ", " << @location.join(", ") if @location.first
304
+ s << ", " << @capability.join(", ") if @capability.first
305
+ s << ", #{@nonstandard.join(", ")}" if @nonstandard.first
306
+ s
307
+ end
308
+
309
+ def encode #:nodoc:
310
+ value = to_str.strip
311
+
312
+ if value.length < 1
313
+ raise InvalidEncodingError, "TEL must have a value"
314
+ end
315
+
316
+ params = [ @location, @capability, @nonstandard ]
317
+ params << 'pref' if @preferred
318
+
319
+ params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
320
+
321
+ paramshash = {}
322
+
323
+ paramshash['TYPE'] = params if params.first
324
+
325
+ Vpim::DirectoryInfo::Field.create( 'TEL', value, paramshash)
326
+ end
327
+
328
+ def Telephone.decode(field) #:nodoc:
329
+ value = field.to_text.strip
330
+
331
+ if value.length < 1
332
+ raise InvalidEncodingError, "TEL must have a value"
333
+ end
334
+
335
+ tel = Telephone.new(value)
336
+
337
+ params = field.pvalues('TYPE')
338
+
339
+ if params
340
+ params.each do |p|
341
+ p.downcase!
342
+ case p
343
+ when 'home', 'work', 'cell', 'car', 'pager'
344
+ tel.location << p
345
+ when 'voice', 'fax', 'video', 'msg', 'bbs', 'modem', 'isdn', 'pcs'
346
+ tel.capability << p
347
+ when 'pref'
348
+ tel.preferred = true
349
+ else
350
+ tel.nonstandard << p
351
+ end
352
+ end
353
+ # Strip duplicates
354
+ [ tel.location, tel.capability, tel.nonstandard ].each do |a|
355
+ a.uniq!
356
+ end
357
+ end
358
+
359
+ tel
360
+ end
361
+ end
362
+
363
+ # The name from a vCard, including all the components of the N: and FN:
364
+ # fields.
365
+ class Name
366
+ # family name, from N
367
+ attr_accessor :family
368
+ # given name, from N
369
+ attr_accessor :given
370
+ # additional names, from N
371
+ attr_accessor :additional
372
+ # such as "Ms." or "Dr.", from N
373
+ attr_accessor :prefix
374
+ # such as "BFA", from N
375
+ attr_accessor :suffix
376
+ # full name, the FN field. FN is a formatted version of the N field,
377
+ # intended to be in a form more aligned with the cultural conventions of
378
+ # the vCard owner than +formatted+ is.
379
+ attr_accessor :fullname
380
+ # all the components of N formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}"
381
+ attr_reader :formatted
382
+
383
+ # Override the attr reader to make it dynamic
384
+ remove_method :formatted
385
+ def formatted #:nodoc:
386
+ f = [ @prefix, @given, @additional, @family ].map{|i| i == '' ? nil : i.strip}.compact.join(' ')
387
+ if @suffix != ''
388
+ f << ', ' << @suffix
389
+ end
390
+ f
391
+ end
392
+
393
+ def initialize(n='', fn='') #:nodoc:
394
+ n = Vpim.decode_text_list(n, ';') do |item|
395
+ item.strip
396
+ end
397
+
398
+ @family = n[0] || ""
399
+ @given = n[1] || ""
400
+ @additional = n[2] || ""
401
+ @prefix = n[3] || ""
402
+ @suffix = n[4] || ""
403
+
404
+ # FIXME - make calls to #fullname fail if fn is nil
405
+ @fullname = (fn || "").strip
406
+ end
407
+
408
+ def encode #:nodoc:
409
+ Vpim::DirectoryInfo::Field.create('N',
410
+ Vpim.encode_text_list([ @family, @given, @additional, @prefix, @suffix ].map{|n| n.strip}, ';')
411
+ )
412
+ end
413
+ def encode_fn #:nodoc:
414
+ fn = @fullname.strip
415
+ if @fullname.length == 0
416
+ fn = formatted
417
+ end
418
+ Vpim::DirectoryInfo::Field.create('FN', fn)
419
+ end
420
+
421
+ end
422
+
423
+ def decode_invisible(field) #:nodoc:
424
+ nil
425
+ end
426
+
427
+ def decode_default(field) #:nodoc:
428
+ Line.new( field.group, field.name, field.value )
429
+ end
430
+
431
+ def decode_version(field) #:nodoc:
432
+ Line.new( field.group, field.name, (field.value.to_f * 10).to_i )
433
+ end
434
+
435
+ def decode_text(field) #:nodoc:
436
+ Line.new( field.group, field.name, Vpim.decode_text(field.value_raw) )
437
+ end
438
+
439
+ def decode_n(field) #:nodoc:
440
+ Line.new( field.group, field.name, Name.new(field.value, self['FN']).freeze )
441
+ end
442
+
443
+ def decode_date_or_datetime(field) #:nodoc:
444
+ date = nil
445
+ begin
446
+ date = Vpim.decode_date_to_date(field.value_raw)
447
+ rescue Vpim::InvalidEncodingError
448
+ date = Vpim.decode_date_time_to_datetime(field.value_raw)
449
+ end
450
+ Line.new( field.group, field.name, date )
451
+ end
452
+
453
+ def decode_bday(field) #:nodoc:
454
+ begin
455
+ return decode_date_or_datetime(field)
456
+
457
+ rescue Vpim::InvalidEncodingError
458
+ # Hack around BDAY dates hat are correct in the month and day, but have
459
+ # some kind of garbage in the year.
460
+ if field.value =~ /^\s*(\d+)-(\d+)-(\d+)\s*$/
461
+ y = $1.to_i
462
+ m = $2.to_i
463
+ d = $3.to_i
464
+ if(y < 1900)
465
+ y = Time.now.year
466
+ end
467
+ Line.new( field.group, field.name, Date.new(y, m, d) )
468
+ else
469
+ raise
470
+ end
471
+ end
472
+ end
473
+
474
+ def decode_geo(field) #:nodoc:
475
+ geo = Vpim.decode_list(field.value_raw, ';') do |item| item.to_f end
476
+ Line.new( field.group, field.name, geo )
477
+ end
478
+
479
+ def decode_address(field) #:nodoc:
480
+ Line.new( field.group, field.name, Address.decode(self, field) )
481
+ end
482
+
483
+ def decode_email(field) #:nodoc:
484
+ Line.new( field.group, field.name, Email.decode(field) )
485
+ end
486
+
487
+ def decode_telephone(field) #:nodoc:
488
+ Line.new( field.group, field.name, Telephone.decode(field) )
489
+ end
490
+
491
+ def decode_list_of_text(field) #:nodoc:
492
+ Line.new( field.group, field.name,
493
+ Vpim.decode_text_list(field.value_raw).select{|t| t.length > 0}.uniq
494
+ )
495
+ end
496
+
497
+ def decode_structured_text(field) #:nodoc:
498
+ Line.new( field.group, field.name, Vpim.decode_text_list(field.value_raw, ';') )
499
+ end
500
+
501
+ def decode_uri(field) #:nodoc:
502
+ Line.new( field.group, field.name, Attachment::Uri.new(field.value, nil) )
503
+ end
504
+
505
+ def decode_agent(field) #:nodoc:
506
+ case field.kind
507
+ when 'text'
508
+ decode_text(field)
509
+ when 'uri'
510
+ decode_uri(field)
511
+ when 'vcard', nil
512
+ Line.new( field.group, field.name, Vcard.decode(Vpim.decode_text(field.value_raw)).first )
513
+ else
514
+ raise InvalidEncodingError, "AGENT type #{field.kind} is not allowed"
515
+ end
516
+ end
517
+
518
+ def decode_attachment(field) #:nodoc:
519
+ Line.new( field.group, field.name, Attachment.decode(field, 'binary', 'TYPE') )
520
+ end
521
+
522
+ @@decode = {
523
+ 'BEGIN' => :decode_invisible, # Don't return delimiter
524
+ 'END' => :decode_invisible, # Don't return delimiter
525
+ 'FN' => :decode_invisible, # Returned as part of N.
526
+
527
+ 'ADR' => :decode_address,
528
+ 'AGENT' => :decode_agent,
529
+ 'BDAY' => :decode_bday,
530
+ 'CATEGORIES' => :decode_list_of_text,
531
+ 'EMAIL' => :decode_email,
532
+ 'GEO' => :decode_geo,
533
+ 'KEY' => :decode_attachment,
534
+ 'LOGO' => :decode_attachment,
535
+ 'MAILER' => :decode_text,
536
+ 'N' => :decode_n,
537
+ 'NAME' => :decode_text,
538
+ 'NICKNAME' => :decode_list_of_text,
539
+ 'NOTE' => :decode_text,
540
+ 'ORG' => :decode_structured_text,
541
+ 'PHOTO' => :decode_attachment,
542
+ 'PRODID' => :decode_text,
543
+ 'PROFILE' => :decode_text,
544
+ 'REV' => :decode_date_or_datetime,
545
+ 'ROLE' => :decode_text,
546
+ 'SOUND' => :decode_attachment,
547
+ 'SOURCE' => :decode_text,
548
+ 'TEL' => :decode_telephone,
549
+ 'TITLE' => :decode_text,
550
+ 'UID' => :decode_text,
551
+ 'URL' => :decode_uri,
552
+ 'VERSION' => :decode_version,
553
+ }
554
+
555
+ @@decode.default = :decode_default
556
+
557
+ # Cache of decoded lines/fields, so we don't have to decode a field more than once.
558
+ attr_reader :cache #:nodoc:
559
+
560
+ # An entry in a vCard. The #value object's type varies with the kind of
561
+ # line (the #name), and on how the line was encoded. The objects returned
562
+ # for a specific kind of line are often extended so that they support a
563
+ # common set of methods. The goal is to allow all types of objects for a
564
+ # kind of line to be treated with some uniformity, but still allow specific
565
+ # handling for the various value types if desired.
566
+ #
567
+ # See the specific methods for details.
568
+ class Line
569
+ attr_reader :group
570
+ attr_reader :name
571
+ attr_reader :value
572
+
573
+ def initialize(group, name, value) #:nodoc:
574
+ @group, @name, @value = (group||''), name.to_str, value
575
+ end
576
+
577
+ def self.decode(decode, card, field) #:nodoc:
578
+ card.cache[field] || (card.cache[field] = card.send(decode[field.name], field))
579
+ end
580
+ end
581
+
582
+ #@lines = {} FIXME - dead code
583
+
584
+ # Return line for a field
585
+ def f2l(field) #:nodoc:
586
+ begin
587
+ Line.decode(@@decode, self, field)
588
+ rescue InvalidEncodingError
589
+ # Skip invalidly encoded fields.
590
+ end
591
+ end
592
+
593
+ # With no block, returns an Array of Line. If +name+ is specified, the
594
+ # Array will only contain the +Line+s with that +name+. The Array may be
595
+ # empty.
596
+ #
597
+ # If a block is given, each Line will be yielded instead of being returned
598
+ # in an Array.
599
+ def lines(name=nil) #:yield: Line
600
+ # FIXME - this would be much easier if #lines was #each, and there was a
601
+ # different #lines that returned an Enumerator that used #each
602
+ unless block_given?
603
+ map do |f|
604
+ if( !name || f.name?(name) )
605
+ f2l(f)
606
+ else
607
+ nil
608
+ end
609
+ end.compact
610
+ else
611
+ each do |f|
612
+ if( !name || f.name?(name) )
613
+ line = f2l(f)
614
+ if line
615
+ yield line
616
+ end
617
+ end
618
+ end
619
+ self
620
+ end
621
+ end
622
+
623
+ private_class_method :new
624
+
625
+ def initialize(fields, profile) #:nodoc:
626
+ @cache = {}
627
+ super(fields, profile)
628
+ end
629
+
630
+ # Create a vCard 3.0 object with the minimum required fields, plus any
631
+ # +fields+ you want in the card (they can also be added later).
632
+ def Vcard.create(fields = [] )
633
+ fields.unshift Field.create('VERSION', "3.0")
634
+ super(fields, 'VCARD')
635
+ end
636
+
637
+ # Decode a collection of vCards into an array of Vcard objects.
638
+ #
639
+ # +card+ can be either a String or an IO object.
640
+ #
641
+ # Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard),
642
+ # multiple vCards can be concatenated into a single directory info object.
643
+ # They may or may not be related. For example, AddressBook.app (the OS X
644
+ # contact manager) will export multiple selected cards in this format.
645
+ #
646
+ # Input data will be converted from unicode if it is detected. The heuristic
647
+ # is based on the first bytes in the string:
648
+ # - 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped
649
+ # - 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string
650
+ # is converted to UTF-8
651
+ # - 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string
652
+ # is converted to UTF-8
653
+ # - 0x00 'B' or 0x00 'b': UTF-16 (big-endian), the string is converted to UTF-8
654
+ # - 'B' 0x00 or 'b' 0x00: UTF-16 (little-endian), the string is converted to UTF-8
655
+ #
656
+ # If you know that you have only one vCard, then you can decode that
657
+ # single vCard by doing something like:
658
+ #
659
+ # vcard = Vcard.decode(card_data).first
660
+ #
661
+ # Note: Should the import encoding be remembered, so that it can be reencoded in
662
+ # the same format?
663
+ def Vcard.decode(card)
664
+ if card.respond_to? :to_str
665
+ string = card.to_str
666
+ elsif card.respond_to? :read
667
+ string = card.read(nil)
668
+ else
669
+ raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
670
+ end
671
+
672
+ case string
673
+ when /^\xEF\xBB\xBF/
674
+ string = string.sub("\xEF\xBB\xBF", '')
675
+ when /^\xFE\xFF/
676
+ arr = string.unpack('n*')
677
+ arr.shift
678
+ string = arr.pack('U*')
679
+ when /^\xFF\xFE/
680
+ arr = string.unpack('v*')
681
+ arr.shift
682
+ string = arr.pack('U*')
683
+ when /^\x00B/i
684
+ string = string.unpack('n*').pack('U*')
685
+ when /^B\x00/i
686
+ string = string.unpack('v*').pack('U*')
687
+ end
688
+
689
+ entities = Vpim.expand(Vpim.decode(string))
690
+
691
+ # Since all vCards must have a begin/end, the top-level should consist
692
+ # entirely of entities/arrays, even if its a single vCard.
693
+ if entities.detect { |e| ! e.kind_of? Array }
694
+ raise "Not a valid vCard"
695
+ end
696
+
697
+ vcards = []
698
+
699
+ for e in entities
700
+ vcards.push(new(e.flatten, 'VCARD'))
701
+ end
702
+
703
+ vcards
704
+ end
705
+
706
+ # The value of the field named +name+, optionally limited to fields of
707
+ # type +type+. If no match is found, nil is returned, if multiple matches
708
+ # are found, the first match to have one of its type values be 'PREF'
709
+ # (preferred) is returned, otherwise the first match is returned.
710
+ #
711
+ # FIXME - this will become an alias for #value.
712
+ def [](name, type=nil)
713
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
714
+
715
+ valued = fields.select { |f| f.value != '' }
716
+ if valued.first
717
+ fields = valued
718
+ end
719
+
720
+ # limit to preferred, if possible
721
+ pref = fields.select { |f| f.pref? }
722
+
723
+ if pref.first
724
+ fields = pref
725
+ end
726
+
727
+ fields.first ? fields.first.value : nil
728
+ end
729
+
730
+ # Return the Line#value for a specific +name+, and optionally for a
731
+ # specific +type+.
732
+ #
733
+ # If no line with the +name+ (and, optionally, +type+) exists, nil is
734
+ # returned.
735
+ #
736
+ # If multiple lines exist, the order of preference is:
737
+ # - lines with values over lines without
738
+ # - lines with a type of 'pref' over lines without
739
+ # If multiple lines are equally preferred, then the first line will be
740
+ # returned.
741
+ #
742
+ # This is most useful when looking for a line that can not occur multiple
743
+ # times, or when the line can occur multiple times, and you want to pick
744
+ # the first preferred line of a specific type. See #values if you need to
745
+ # access all the lines.
746
+ #
747
+ # Note that the +type+ field parameter is used for different purposes by
748
+ # the various kinds of vCard lines, but for the addressing lines (ADR,
749
+ # LABEL, TEL, EMAIL) it is has a reasonably consistent usage. Each
750
+ # addressing line can occur multiple times, and a +type+ of 'pref'
751
+ # indicates that a particular line is the preferred line. Other +type+
752
+ # values tend to indicate some information about the location ('home',
753
+ # 'work', ...) or some detail about the address ('cell', 'fax', 'voice',
754
+ # ...). See the methods for the specific types of line for information
755
+ # about supported types and their meaning.
756
+ def value(name, type = nil)
757
+ v = nil
758
+
759
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
760
+
761
+ valued = fields.select { |f| f.value != '' }
762
+ if valued.first
763
+ fields = valued
764
+ end
765
+
766
+ pref = fields.select { |f| f.pref? }
767
+
768
+ if pref.first
769
+ fields = pref
770
+ end
771
+
772
+ if fields.first
773
+ line = begin
774
+ Line.decode(@@decode, self, fields.first)
775
+ rescue Vpim::InvalidEncodingError
776
+ end
777
+
778
+ if line
779
+ return line.value
780
+ end
781
+ end
782
+
783
+ nil
784
+ end
785
+
786
+ # A variant of #lines that only iterates over specific Line names. Since
787
+ # the name is known, only the Line#value is returned or yielded.
788
+ def values(name)
789
+ unless block_given?
790
+ lines(name).map { |line| line.value }
791
+ else
792
+ lines(name) { |line| yield line.value }
793
+ end
794
+ end
795
+
796
+ # The first ADR value of type +type+, a Address. Any of the location or
797
+ # delivery attributes of Address can be used as +type+. A wrapper around
798
+ # #value('ADR', +type+).
799
+ def address(type=nil)
800
+ value('ADR', type)
801
+ end
802
+
803
+ # The ADR values, an array of Address. If a block is given, the values are
804
+ # yielded. A wrapper around #values('ADR').
805
+ def addresses #:yield:address
806
+ values('ADR')
807
+ end
808
+
809
+ # The AGENT values. Each AGENT value is either a String, a Uri, or a Vcard.
810
+ # If a block is given, the values are yielded. A wrapper around
811
+ # #values('AGENT').
812
+ def agents #:yield:agent
813
+ values('AGENT')
814
+ end
815
+
816
+ # The BDAY value as either a Date or a DateTime, or nil if there is none.
817
+ #
818
+ # If the BDAY value is invalidly formatted, a feeble heuristic is applied
819
+ # to find the month and year, and return a Date in the current year.
820
+ def birthday
821
+ value('BDAY')
822
+ end
823
+
824
+ # The CATEGORIES values, an array of String. A wrapper around
825
+ # #value('CATEGORIES').
826
+ def categories
827
+ value('CATEGORIES')
828
+ end
829
+
830
+ # The first EMAIL value of type +type+, a Email. Any of the location
831
+ # attributes of Email can be used as +type+. A wrapper around
832
+ # #value('EMAIL', +type+).
833
+ def email(type=nil)
834
+ value('EMAIL', type)
835
+ end
836
+
837
+ # The EMAIL values, an array of Email. If a block is given, the values are
838
+ # yielded. A wrapper around #values('EMAIL').
839
+ def emails #:yield:email
840
+ values('EMAIL')
841
+ end
842
+
843
+ # The GEO value, an Array of two Floats, +[ latitude, longitude]+. North
844
+ # of the equator is positive latitude, east of the meridian is positive
845
+ # longitude. See RFC2445 for more info, there are lots of special cases
846
+ # and RFC2445's description is more complete thant RFC2426.
847
+ def geo
848
+ value('GEO')
849
+ end
850
+
851
+ # Return an Array of KEY Line#value, or yield each Line#value if a block
852
+ # is given. A wrapper around #values('KEY').
853
+ #
854
+ # KEY is a public key or authentication certificate associated with the
855
+ # object that the vCard represents. It is not commonly used, but could
856
+ # contain a X.509 or PGP certificate.
857
+ #
858
+ # See Attachment for a description of the value.
859
+ def keys(&proc) #:yield: Line.value
860
+ values('KEY', &proc)
861
+ end
862
+
863
+ # Return an Array of LOGO Line#value, or yield each Line#value if a block
864
+ # is given. A wrapper around #values('LOGO').
865
+ #
866
+ # LOGO is a graphic image of a logo associated with the object the vCard
867
+ # represents. Its not common, but would probably be equivalent to the logo
868
+ # on a printed card.
869
+ #
870
+ # See Attachment for a description of the value.
871
+ def logos(&proc) #:yield: Line.value
872
+ values('LOGO', &proc)
873
+ end
874
+
875
+ ## MAILER
876
+
877
+ # The N and FN as a Name object.
878
+ #
879
+ # N is required for a vCards, this raises InvalidEncodingError if
880
+ # there is no N so it cannot return nil.
881
+ def name
882
+ value('N') || raise(Vpim::InvalidEncodingError, "Missing mandatory N field")
883
+ end
884
+
885
+ # The first NICKNAME value, nil if there are none.
886
+ def nickname
887
+ v = value('NICKNAME')
888
+ v = v.first if v
889
+ v
890
+ end
891
+
892
+ # The NICKNAME values, an array of String. The array may be empty.
893
+ def nicknames
894
+ values('NICKNAME').flatten.uniq
895
+ end
896
+
897
+ # The NOTE value, a String. A wrapper around #value('NOTE').
898
+ def note
899
+ value('NOTE')
900
+ end
901
+
902
+ # The ORG value, an Array of String. The first string is the organization,
903
+ # subsequent strings are departments within the organization. A wrapper
904
+ # around #value('ORG').
905
+ def org
906
+ value('ORG')
907
+ end
908
+
909
+ # Return an Array of PHOTO Line#value, or yield each Line#value if a block
910
+ # is given. A wrapper around #values('PHOTO').
911
+ #
912
+ # PHOTO is an image or photograph information that annotates some aspect of
913
+ # the object the vCard represents. Commonly there is one PHOTO, and it is a
914
+ # photo of the person identified by the vCard.
915
+ #
916
+ # See Attachment for a description of the value.
917
+ def photos(&proc) #:yield: Line.value
918
+ values('PHOTO', &proc)
919
+ end
920
+
921
+ ## PRODID
922
+
923
+ ## PROFILE
924
+
925
+ ## REV
926
+
927
+ ## ROLE
928
+
929
+ # Return an Array of SOUND Line#value, or yield each Line#value if a block
930
+ # is given. A wrapper around #values('SOUND').
931
+ #
932
+ # SOUND is digital sound content information that annotates some aspect of
933
+ # the vCard. By default this type is used to specify the proper
934
+ # pronunciation of the name associated with the vCard. It is not commonly
935
+ # used. Also, note that there is no mechanism available to specify that the
936
+ # SOUND is being used for anything other than the default.
937
+ #
938
+ # See Attachment for a description of the value.
939
+ def sounds(&proc) #:yield: Line.value
940
+ values('SOUND', &proc)
941
+ end
942
+
943
+ ## SOURCE
944
+
945
+ # The first TEL value of type +type+, a Telephone. Any of the location or
946
+ # capability attributes of Telephone can be used as +type+. A wrapper around
947
+ # #value('TEL', +type+).
948
+ def telephone(type=nil)
949
+ value('TEL', type)
950
+ end
951
+
952
+ # The TEL values, an array of Telephone. If a block is given, the values are
953
+ # yielded. A wrapper around #values('TEL').
954
+ def telephones #:yield:tel
955
+ values('TEL')
956
+ end
957
+
958
+ # The TITLE value, a text string specifying the job title, functional
959
+ # position, or function of the object the card represents. A wrapper around
960
+ # #value('TITLE').
961
+ def title
962
+ value('TITLE')
963
+ end
964
+
965
+ ## UID
966
+
967
+ # The URL value, a Attachment::Uri. A wrapper around #value('URL').
968
+ def url
969
+ value('URL')
970
+ end
971
+
972
+ # The URL values, an Attachment::Uri. A wrapper around #values('URL').
973
+ def urls
974
+ values('URL')
975
+ end
976
+
977
+ # The VERSION multiplied by 10 as an Integer. For example, a VERSION:2.1
978
+ # vCard would have a version of 21, and a VERSION:3.0 vCard would have a
979
+ # version of 30.
980
+ #
981
+ # VERSION is required for a vCard, this raises InvalidEncodingError if
982
+ # there is no VERSION so it cannot return nil.
983
+ def version
984
+ v = value('VERSION')
985
+ unless v
986
+ raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
987
+ end
988
+ v
989
+ end
990
+
991
+ # Make changes to a vCard.
992
+ #
993
+ # Yields a Vpim::Vcard::Maker that can be used to modify this vCard.
994
+ def make #:yield: maker
995
+ Vpim::Vcard::Maker.make2(self) do |maker|
996
+ yield maker
997
+ end
998
+ end
999
+
1000
+ # Delete +line+ if block yields true.
1001
+ def delete_if #:nodoc: :yield: line
1002
+ # Do in two steps to not mess up progress through the enumerator.
1003
+ rm = []
1004
+
1005
+ each do |f|
1006
+ line = f2l(f)
1007
+ if line && yield(line)
1008
+ rm << f
1009
+
1010
+ # Hack - because we treat N and FN as one field
1011
+ if f.name? 'N'
1012
+ rm << field('FN')
1013
+ end
1014
+ end
1015
+ end
1016
+
1017
+ rm.each do |f|
1018
+ @fields.delete( f )
1019
+ @cache.delete( f )
1020
+ end
1021
+
1022
+ end
1023
+
1024
+ # A class to make and make changes to vCards.
1025
+ #
1026
+ # It can be used to create completely new vCards using Vcard#make2.
1027
+ #
1028
+ # Its is also yielded from Vpim::Vcard#make, in which case it allows a kind
1029
+ # of transactional approach to changing vCards, so their values can be
1030
+ # validated after any changes have been made.
1031
+ #
1032
+ # Examples:
1033
+ # - link:ex_mkvcard.txt: example of creating a vCard
1034
+ # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
1035
+ # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
1036
+ # - link:ex_mkyourown.txt: example of adding support for new fields to Vcard::Maker
1037
+ class Maker
1038
+ # Make a vCard.
1039
+ #
1040
+ # Yields +maker+, a Vpim::Vcard::Maker which allows fields to be added to
1041
+ # +card+, and returns +card+, a Vpim::Vcard.
1042
+ #
1043
+ # If +card+ is nil or not provided a new Vpim::Vcard is created and the
1044
+ # fields are added to it.
1045
+ #
1046
+ # Defaults:
1047
+ # - vCards must have both an N and an FN field, #make2 will fail if there
1048
+ # is no N field in the +card+ when your block is finished adding fields.
1049
+ # - If there is an N field, but no FN field, FN will be set from the
1050
+ # information in N, see Vcard::Name#preformatted for more information.
1051
+ # - vCards must have a VERSION field. If one does not exist when your block is
1052
+ # is finished it will be set to 3.0.
1053
+ def self.make2(card = Vpim::Vcard.create, &block) # :yields: maker
1054
+ new(nil, card).make(&block)
1055
+ end
1056
+
1057
+ # Deprecated, use #make2.
1058
+ #
1059
+ # If set, the FN field will be set to +full_name+. Otherwise, FN will
1060
+ # be set from the values in #name.
1061
+ def self.make(full_name = nil, &block) # :yields: maker
1062
+ new(full_name, Vpim::Vcard.create).make(&block)
1063
+ end
1064
+
1065
+ def make # :nodoc:
1066
+ yield self
1067
+ unless @card['N']
1068
+ raise Unencodeable, 'N field is mandatory'
1069
+ end
1070
+ fn = @card.field('FN')
1071
+ if fn && fn.value.strip.length == 0
1072
+ @card.delete(fn)
1073
+ fn = nil
1074
+ end
1075
+ unless fn
1076
+ @card << Vpim::DirectoryInfo::Field.create('FN', Vpim::Vcard::Name.new(@card['N'], '').formatted)
1077
+ end
1078
+ unless @card['VERSION']
1079
+ @card << Vpim::DirectoryInfo::Field.create('VERSION', "3.0")
1080
+ end
1081
+ @card
1082
+ end
1083
+
1084
+ private
1085
+
1086
+ def initialize(full_name, card) # :nodoc:
1087
+ @card = card || Vpim::Vcard::create
1088
+ if full_name
1089
+ @card << Vpim::DirectoryInfo::Field.create('FN', full_name.strip )
1090
+ end
1091
+ end
1092
+
1093
+ public
1094
+
1095
+ # Deprecated, see #name.
1096
+ #
1097
+ # Use
1098
+ # maker.name do |n| n.fullname = "foo" end
1099
+ # to set just fullname, or set the other fields to set fullname and the
1100
+ # name.
1101
+ def fullname=(fullname) #:nodoc: bacwards compat
1102
+ if @card.field('FN')
1103
+ raise Vpim::InvalidEncodingError, "Not allowed to add more than one FN field to a vCard."
1104
+ end
1105
+ @card << Vpim::DirectoryInfo::Field.create( 'FN', fullname );
1106
+ end
1107
+
1108
+ # Set the name fields, N and FN.
1109
+ #
1110
+ # Attributes of +name+ are:
1111
+ # - family: family name
1112
+ # - given: given name
1113
+ # - additional: additional names
1114
+ # - prefix: such as "Ms." or "Dr."
1115
+ # - suffix: such as "BFA", or "Sensei"
1116
+ #
1117
+ # +name+ is a Vcard::Name.
1118
+ #
1119
+ # All attributes are optional, though have all names be zero-length
1120
+ # strings isn't really in the spirit of things. FN's value will be set
1121
+ # to Vcard::Name#formatted if Vcard::Name#fullname isn't given a specific
1122
+ # value.
1123
+ #
1124
+ # Warning: This is the only mandatory field.
1125
+ def name #:yield:name
1126
+ x = begin
1127
+ @card.name.dup
1128
+ rescue
1129
+ Vpim::Vcard::Name.new
1130
+ end
1131
+
1132
+ fn = x.fullname
1133
+
1134
+ yield x
1135
+
1136
+ x.fullname.strip!
1137
+
1138
+ delete_if do |line|
1139
+ line.name == 'N'
1140
+ end
1141
+
1142
+ @card << x.encode
1143
+ @card << x.encode_fn
1144
+
1145
+ self
1146
+ end
1147
+
1148
+ alias :add_name :name #:nodoc: backwards compatibility
1149
+
1150
+ # Add an address field, ADR. +address+ is a Vpim::Vcard::Address.
1151
+ def add_addr # :yield: address
1152
+ x = Vpim::Vcard::Address.new
1153
+ yield x
1154
+ @card << x.encode
1155
+ self
1156
+ end
1157
+
1158
+ # Add a telephone field, TEL. +tel+ is a Vpim::Vcard::Telephone.
1159
+ #
1160
+ # The block is optional, its only necessary if you want to specify
1161
+ # the optional attributes.
1162
+ def add_tel(number) # :yield: tel
1163
+ x = Vpim::Vcard::Telephone.new(number)
1164
+ if block_given?
1165
+ yield x
1166
+ end
1167
+ @card << x.encode
1168
+ self
1169
+ end
1170
+
1171
+ # Add an email field, EMAIL. +email+ is a Vpim::Vcard::Email.
1172
+ #
1173
+ # The block is optional, its only necessary if you want to specify
1174
+ # the optional attributes.
1175
+ def add_email(email) # :yield: email
1176
+ x = Vpim::Vcard::Email.new(email)
1177
+ if block_given?
1178
+ yield x
1179
+ end
1180
+ @card << x.encode
1181
+ self
1182
+ end
1183
+
1184
+ # Set the nickname field, NICKNAME.
1185
+ #
1186
+ # It can be set to a single String or an Array of String.
1187
+ def nickname=(nickname)
1188
+ delete_if { |l| l.name == 'NICKNAME' }
1189
+
1190
+ @card << Vpim::DirectoryInfo::Field.create( 'NICKNAME', nickname );
1191
+ end
1192
+
1193
+ # Add a birthday field, BDAY.
1194
+ #
1195
+ # +birthday+ must be a time or date object.
1196
+ #
1197
+ # Warning: It may confuse both humans and software if you add multiple
1198
+ # birthdays.
1199
+ def birthday=(birthday)
1200
+ if !birthday.respond_to? :month
1201
+ raise ArgumentError, 'birthday must be a date or time object.'
1202
+ end
1203
+ delete_if { |l| l.name == 'BDAY' }
1204
+ @card << Vpim::DirectoryInfo::Field.create( 'BDAY', birthday );
1205
+ end
1206
+
1207
+ # Add a note field, NOTE. The +note+ String can contain newlines, they
1208
+ # will be escaped.
1209
+ def add_note(note)
1210
+ @card << Vpim::DirectoryInfo::Field.create( 'NOTE', Vpim.encode_text(note) );
1211
+ end
1212
+
1213
+ # Add an instant-messaging/point of presence address field, IMPP. The address
1214
+ # is a URL, with the syntax depending on the protocol.
1215
+ #
1216
+ # Attributes of IMPP are:
1217
+ # - preferred: true - set if this is the preferred address
1218
+ # - location: home, work, mobile - location of address
1219
+ # - purpose: personal,business - purpose of communications
1220
+ #
1221
+ # All attributes are optional, and so is the block.
1222
+ #
1223
+ # The URL syntaxes for the messaging schemes is fairly complicated, so I
1224
+ # don't try and build the URLs here, maybe in the future. This forces
1225
+ # the user to know the URL for their own address, hopefully not too much
1226
+ # of a burden.
1227
+ #
1228
+ # IMPP is defined in draft-jennings-impp-vcard-04.txt. It refers to the
1229
+ # URI scheme of a number of messaging protocols, but doesn't give
1230
+ # references to all of them:
1231
+ # - "xmpp" indicates to use XMPP, draft-saintandre-xmpp-uri-06.txt
1232
+ # - "irc" or "ircs" indicates to use IRC, draft-butcher-irc-url-04.txt
1233
+ # - "sip" indicates to use SIP/SIMPLE, RFC 3261
1234
+ # - "im" or "pres" indicates to use a CPIM or CPP gateway, RFC 3860 and RFC 3859
1235
+ # - "ymsgr" indicates to use yahoo
1236
+ # - "msn" might indicate to use Microsoft messenger
1237
+ # - "aim" indicates to use AOL
1238
+ #
1239
+ def add_impp(url) # :yield: impp
1240
+ params = {}
1241
+
1242
+ if block_given?
1243
+ x = Struct.new( :location, :preferred, :purpose ).new
1244
+
1245
+ yield x
1246
+
1247
+ x[:preferred] = 'PREF' if x[:preferred]
1248
+
1249
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1250
+
1251
+ params['TYPE'] = types if types.first
1252
+ end
1253
+
1254
+ @card << Vpim::DirectoryInfo::Field.create( 'IMPP', url, params)
1255
+ self
1256
+ end
1257
+
1258
+ # Add an X-AIM account name where +xaim+ is an AIM screen name.
1259
+ #
1260
+ # I don't know if this is conventional, or supported by anything other
1261
+ # than AddressBook.app, but an example is:
1262
+ # X-AIM;type=HOME;type=pref:exampleaccount
1263
+ #
1264
+ # Attributes of X-AIM are:
1265
+ # - preferred: true - set if this is the preferred address
1266
+ # - location: home, work, mobile - location of address
1267
+ #
1268
+ # All attributes are optional, and so is the block.
1269
+ def add_x_aim(xaim) # :yield: xaim
1270
+ params = {}
1271
+
1272
+ if block_given?
1273
+ x = Struct.new( :location, :preferred ).new
1274
+
1275
+ yield x
1276
+
1277
+ x[:preferred] = 'PREF' if x[:preferred]
1278
+
1279
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1280
+
1281
+ params['TYPE'] = types if types.first
1282
+ end
1283
+
1284
+ @card << Vpim::DirectoryInfo::Field.create( 'X-AIM', xaim, params)
1285
+ self
1286
+ end
1287
+
1288
+
1289
+ # Add a photo field, PHOTO.
1290
+ #
1291
+ # Attributes of PHOTO are:
1292
+ # - image: set to image data to include inline
1293
+ # - link: set to the URL of the image data
1294
+ # - type: string identifying the image type, supposed to be an "IANA registered image format",
1295
+ # or a non-registered image format (usually these start with an x-)
1296
+ #
1297
+ # An error will be raised if neither image or link is set, or if both image
1298
+ # and link is set.
1299
+ #
1300
+ # Setting type is optional for a link image, because either the URL, the
1301
+ # image file extension, or a HTTP Content-Type may specify the type. If
1302
+ # it's not a link, setting type is mandatory, though it can be set to an
1303
+ # empty string, <code>''</code>, if the type is unknown.
1304
+ #
1305
+ # TODO - I'm not sure about this API. I'm thinking maybe it should be
1306
+ # #add_photo(image, type), and that I should detect when the image is a
1307
+ # URL, and make type mandatory if it wasn't a URL.
1308
+ def add_photo # :yield: photo
1309
+ x = Struct.new(:image, :link, :type).new
1310
+ yield x
1311
+ if x[:image] && x[:link]
1312
+ raise Vpim::InvalidEncodingError, 'Image is not allowed to be both inline and a link.'
1313
+ end
1314
+
1315
+ value = x[:image] || x[:link]
1316
+
1317
+ if !value
1318
+ raise Vpim::InvalidEncodingError, 'A image link or inline data must be provided.'
1319
+ end
1320
+
1321
+ params = {}
1322
+
1323
+ # Don't set type to the empty string.
1324
+ params['TYPE'] = x[:type] if( x[:type] && x[:type].length > 0 )
1325
+
1326
+ if x[:link]
1327
+ params['VALUE'] = 'URI'
1328
+ else # it's inline, base-64 encode it
1329
+ params['ENCODING'] = :b64
1330
+ if !x[:type]
1331
+ raise Vpim::InvalidEncodingError, 'Inline image data must have it\'s type set.'
1332
+ end
1333
+ end
1334
+
1335
+ @card << Vpim::DirectoryInfo::Field.create( 'PHOTO', value, params )
1336
+ self
1337
+ end
1338
+
1339
+ # Set the title field, TITLE.
1340
+ #
1341
+ # It can be set to a single String.
1342
+ def title=(title)
1343
+ delete_if { |l| l.name == 'TITLE' }
1344
+
1345
+ @card << Vpim::DirectoryInfo::Field.create( 'TITLE', title );
1346
+ end
1347
+
1348
+ # for adding a job title
1349
+ def add_title(value)
1350
+ @card << Vpim::DirectoryInfo::Field.create( 'TITLE', value.to_str );
1351
+ end
1352
+
1353
+ # Set the org field, ORG.
1354
+ #
1355
+ # It can be set to a single String or an Array of String.
1356
+ def org=(org)
1357
+ delete_if { |l| l.name == 'ORG' }
1358
+
1359
+ @card << Vpim::DirectoryInfo::Field.create( 'ORG', org );
1360
+ end
1361
+
1362
+ # for adding a company
1363
+ def add_org(value)
1364
+ @card << Vpim::DirectoryInfo::Field.create( 'ORG', value.to_str );
1365
+ end
1366
+
1367
+ # Add a URL field, URL.
1368
+ # Microsoft Outlook is not following the vCard 3.0 specs
1369
+ def add_url(url)
1370
+ params = {}
1371
+
1372
+ if block_given?
1373
+ x = Struct.new( :location, :preferred, :purpose ).new
1374
+
1375
+ yield x
1376
+
1377
+ x[:preferred] = 'PREF' if x[:preferred]
1378
+
1379
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1380
+
1381
+ params['TYPE'] = types if types.first
1382
+ end
1383
+
1384
+ @card << Vpim::DirectoryInfo::Field.create( 'URL', url.to_str, params)
1385
+ self
1386
+ end
1387
+
1388
+ # Add a Field, +field+.
1389
+ def add_field(field)
1390
+ fieldname = field.name.upcase
1391
+ case
1392
+ when [ 'BEGIN', 'END' ].include?(fieldname)
1393
+ raise Vpim::InvalidEncodingError, "Not allowed to manually add #{field.name} to a vCard."
1394
+
1395
+ when [ 'VERSION', 'N', 'FN' ].include?(fieldname)
1396
+ if @card.field(fieldname)
1397
+ raise Vpim::InvalidEncodingError, "Not allowed to add more than one #{fieldname} to a vCard."
1398
+ end
1399
+ @card << field
1400
+
1401
+ else
1402
+ @card << field
1403
+ end
1404
+ end
1405
+
1406
+ # for marking a vCard as a company
1407
+ def set_as_company
1408
+ @card << Vpim::DirectoryInfo::Field.create( 'X-ABShowAs', 'COMPANY' );
1409
+ end
1410
+
1411
+ # Copy the fields from +card+ into self using #add_field. If a block is
1412
+ # provided, each Field from +card+ is yielded. The block should return a
1413
+ # Field to add, or nil. The Field doesn't have to be the one yielded,
1414
+ # allowing the field to be copied and modified (see Field#copy) before adding, or
1415
+ # not added at all if the block yields nil.
1416
+ #
1417
+ # The vCard fields BEGIN and END aren't copied, and VERSION, N, and FN are copied
1418
+ # only if the card doesn't have them already.
1419
+ def copy(card) # :yields: Field
1420
+ card.each do |field|
1421
+ fieldname = field.name.upcase
1422
+ case
1423
+ when [ 'BEGIN', 'END' ].include?(fieldname)
1424
+ # Never copy these
1425
+
1426
+ when [ 'VERSION', 'N', 'FN' ].include?(fieldname) && @card.field(fieldname)
1427
+ # Copy these only if they don't already exist.
1428
+
1429
+ else
1430
+ if block_given?
1431
+ field = yield field
1432
+ end
1433
+
1434
+ if field
1435
+ add_field(field)
1436
+ end
1437
+ end
1438
+ end
1439
+ end
1440
+
1441
+ # Delete +line+ if block yields true.
1442
+ def delete_if #:yield: line
1443
+ begin
1444
+ @card.delete_if do |line|
1445
+ yield line
1446
+ end
1447
+ rescue NoMethodError
1448
+ # FIXME - this is a hideous hack, allowing a DirectoryInfo to
1449
+ # be passed instead of a Vcard, and for it to almost work. Yuck.
1450
+ end
1451
+ end
1452
+
1453
+ end
1454
+ end
1455
+ end
1456
+