vpim2 0.0.1

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