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