mumboe-vpim 0.7

Sign up to get free protection for your applications and to get access to all the features.
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
+