vpim 0.323 → 0.357

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.
@@ -1,3 +1,11 @@
1
+ =begin
2
+ Copyright (C) 2006 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
+
1
9
  module Vpim
2
10
  class Icalendar
3
11
  module Property
@@ -10,7 +18,8 @@ module Vpim
10
18
 
11
19
  # Array of Float, +[ latitude, longitude]+.
12
20
  #
13
- # North or equator is postive latitude, East of meridian is positive logitude.
21
+ # North of the equator is positive latitude, east of the meridian is
22
+ # positive longitude.
14
23
  #
15
24
  # See RFC2445 for more info... there are lots of special cases.
16
25
  def geo
@@ -1,3 +1,11 @@
1
+ =begin
2
+ Copyright (C) 2006 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
+
1
9
  module Vpim
2
10
  class Icalendar
3
11
  module Property
@@ -0,0 +1,47 @@
1
+ =begin
2
+ Copyright (C) 2006 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
+ module Vpim
10
+ class Icalendar
11
+ module Property
12
+
13
+ # Occurrences are calculated from DTSTART: and RRULE:. If there is not
14
+ # RRULE:, the component recurs only once, at the start time.
15
+ #
16
+ # Limitations:
17
+ #
18
+ # Only a single RRULE: is currently supported, this is the most common
19
+ # case.
20
+ #
21
+ # Implementation of multiple RRULE:s, and RDATE:, EXRULE:, and EXDATE: is
22
+ # on the todo list. Its not a very high priority, because I haven't seen
23
+ # calendars using the full range of recurrence features, and haven't
24
+ # received feedback from any users requesting these features. So, if you
25
+ # need it, contact me and implementation will get on the schedule.
26
+ module Recurrence
27
+ # The times this event occurs, as a Vpim::Rrule.
28
+ def occurences
29
+ start = dtstart
30
+ unless start
31
+ raise ArgumentError, "Components with no DTSTART: don't have occurences!"
32
+ end
33
+ Vpim::Rrule.new(start, propvalue('RRULE'))
34
+ end
35
+
36
+ # Check if this event overlaps with the time period later than or equal to +t0+, but
37
+ # earlier than +t1+.
38
+ def occurs_in?(t0, t1)
39
+ occurences.each_until(t1).detect { |t| tend = t + (duration || 0); tend > t0 }
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+
@@ -1,3 +1,11 @@
1
+ =begin
2
+ Copyright (C) 2006 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
+
1
9
  module Vpim
2
10
  class Icalendar
3
11
  module Property
@@ -53,6 +53,9 @@ module Vpim
53
53
  # time-zone = "Z" / time-numzone
54
54
  # time-numzome = sign time-hour [":"] time-minute
55
55
  TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
56
+
57
+ # integer = (["+"] / "-") 1*DIGIT
58
+ INTEGER = '[-+]?\d+'
56
59
  end
57
60
  end
58
61
 
@@ -148,14 +151,23 @@ module Vpim
148
151
  # float
149
152
  #
150
153
  # float_list
151
- #
152
- # integer
154
+ =begin
155
+ =end
156
+
157
+ # Convert an RFC2425 INTEGER value into an Integer
158
+ def Vpim.decode_integer(v) # :nodoc:
159
+ unless match = %r{\s*#{Bnf::INTEGER}\s*}.match(v)
160
+ raise Vpim::InvalidEncodingError, "integer not valid (#{v})"
161
+ end
162
+ v.to_i
163
+ end
164
+
153
165
  #
154
166
  # integer_list
155
167
  #
156
168
  # text_list
157
169
 
158
- # Convert a RFC 2425 date-list into an array of dates.
170
+ # Convert a RFC2425 date-list into an array of dates.
159
171
  def Vpim.decode_date_list(v) # :nodoc:
160
172
  Vpim.decode_list(v) do |date|
161
173
  date.strip!
@@ -191,24 +203,49 @@ module Vpim
191
203
  # \N -> NL
192
204
  # \, -> ,
193
205
  # \; -> ;
206
+ #
207
+ # I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed
208
+ # to escape anything but the above, everything else is ambiguous, so I'll
209
+ # just support it.
194
210
  def Vpim.decode_text(v) # :nodoc:
211
+ # FIXME - I think this should trim leading and trailing space
195
212
  v.gsub(/\\(.)/) do
196
213
  case $1
197
- when '\\', ',', ';'
198
- $1
199
214
  when 'n', 'N'
200
215
  "\n"
201
216
  else
202
- raise Vpim::InvalidEncodingError, "TEXT #{v.inspect} uses invalid escape sequence '\\#{$1}'"
217
+ $1
203
218
  end
204
219
  end
205
220
  end
221
+
222
+ def Vpim.encode_text(v) #:nodoc:
223
+ v.to_str.gsub(/([.\n])/) do
224
+ case $1
225
+ when "\n"
226
+ "\\n"
227
+ when "\\", ",", ";"
228
+ "\\#{$1}"
229
+ else
230
+ $1
231
+ end
232
+ end
233
+ end
234
+
235
+ def Vpim.encode_text_list(v, sep = ",") #:nodoc:
236
+ v.to_ary.map{ |t| Vpim.encode_text(t) }.join(sep)
237
+ end
206
238
 
207
239
  # Convert a +sep+-seperated list of TEXT values into an array of values.
208
240
  def Vpim.decode_text_list(value, sep = ',') # :nodoc:
209
- value.scan(/((?:(?:\\.)|[^#{sep}])+)#{sep}?/).map do |v|
241
+ # Need to do in two stages, as best I can find.
242
+ list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map do |v|
210
243
  Vpim.decode_text(v.first)
211
244
  end
245
+ if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/)
246
+ list << $1
247
+ end
248
+ list
212
249
  end
213
250
 
214
251
 
@@ -135,7 +135,7 @@ module Vpim
135
135
  # less than +dountil+).
136
136
  #
137
137
  # Also, iteration will not currently continue past the limit of a Time
138
- # object, which is some time in 2037 with the 32-bit time_t's common on
138
+ # object, which is some time in 2037 with the 32-bit time_t common on
139
139
  # most systems.
140
140
  def each(dountil = nil) #:yield: ytime
141
141
  t = @dtstart.clone
@@ -6,8 +6,12 @@
6
6
  details.
7
7
  =end
8
8
 
9
- require 'vpim/dirinfo'
10
9
  require 'vpim/vpim'
10
+ require 'vpim/attachment'
11
+ require 'vpim/dirinfo'
12
+
13
+ require 'open-uri'
14
+ require 'stringio'
11
15
 
12
16
  module Vpim
13
17
  # A vCard, a specialization of a directory info object.
@@ -16,9 +20,31 @@ module Vpim
16
20
  # - RFC2426: vCard MIME Directory Profile (vCard 3.0)
17
21
  # - RFC2425: A MIME Content-Type for Directory Information
18
22
  #
19
- # This implements vCard 3.0, but it is also capable of decoding vCard 2.1.
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.
20
43
  #
21
- # For information about:
44
+ # If there is sufficient demand, specific support for vCard 2.1 could be
45
+ # implemented.
46
+ #
47
+ # For more information see:
22
48
  # - link:rfc2426.txt: vCard MIME Directory Profile (vCard 3.0)
23
49
  # - link:rfc2425.txt: A MIME Content-Type for Directory Information
24
50
  # - http://www.imc.org/pdi/pdiproddev.html: vCard 2.1 Specifications
@@ -26,17 +52,6 @@ module Vpim
26
52
  # vCards are usually transmitted in files with <code>.vcf</code>
27
53
  # extensions.
28
54
  #
29
- # TODO - an open question is what exactly "vcard 2.1" support means. While I
30
- # decode vCard 2.1 correctly, I don't encode it. Should I implement a
31
- # transcoder, so vCards can be decoded from either version, and then written
32
- # to either version? Maybe an option to Field#encode()?
33
- #
34
- # TODO - there are very few methods that Vcard has that DirectoryInfo
35
- # doesn't. I could probably just do away with it entirely, but I suspect
36
- # that there are methods that could be usefully added to Vcard, perhaps to
37
- # get the email addresses, or the name, or perhaps to set fields, like
38
- # email=. What would be useful?
39
- #
40
55
  # = Examples
41
56
  #
42
57
  # - link:ex_mkvcard.txt: example of creating a vCard
@@ -51,18 +66,566 @@ module Vpim
51
66
  # (small but) complete application contributed by Dane G. Avilla, thanks!
52
67
  # - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards
53
68
  # - link:vcf-dump.txt: utility for dumping contents of .vcf files
54
- #
55
- # Here's an example of encoding a simple vCard using the low-level API:
56
- #
57
- # card = Vpim::Vcard.create
58
- # card << Vpim::DirectoryInfo::Field.create('EMAIL', 'user.name@example.com', 'TYPE' => 'INTERNET' )
59
- # card << Vpim::DirectoryInfo::Field.create('URL', 'http://www.example.com/user' )
60
- # card << Vpim::DirectoryInfo::Field.create('FN', 'User Name' )
61
- # puts card.to_s
62
69
  class Vcard < DirectoryInfo
63
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(field.value_raw)
450
+ date = Date.new(*date)
451
+ rescue Vpim::InvalidEncodingError
452
+ # FIXME - try and decode as DATE-TIME
453
+ raise
454
+ end
455
+ Line.new( field.group, field.name, date )
456
+ end
457
+
458
+ def decode_bday(field) #:nodoc:
459
+ begin
460
+ return decode_date_or_datetime(field)
461
+
462
+ rescue Vpim::InvalidEncodingError
463
+ if field.value =~ /(\d+)-(\d+)-(\d+)/
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, Uri.new(field.value) )
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
+ Line.decode(@@decode, self, field)
590
+ end
591
+
592
+ # With no block, returns an Array of Line. If +name+ is specified, the
593
+ # Array will only contain the +Line+s with that +name+. The Array may be
594
+ # empty.
595
+ #
596
+ # If a block is given, each Line will be yielded instead of being returned
597
+ # in an Array.
598
+ def lines(name=nil) #:yield: Line
599
+ # FIXME - this would be much easier if #lines was #each, and there was a
600
+ # different #lines that returned an Enumerator that used #each
601
+ unless block_given?
602
+ map do |f|
603
+ if( !name || f.name?(name) )
604
+ f2l(f)
605
+ else
606
+ nil
607
+ end
608
+ end.compact
609
+ else
610
+ each do |f|
611
+ if( !name || f.name?(name) )
612
+ line = f2l(f)
613
+ if line
614
+ yield line
615
+ end
616
+ end
617
+ end
618
+ self
619
+ end
620
+ end
621
+
64
622
  private_class_method :new
65
623
 
624
+ def initialize(fields, profile) #:nodoc:
625
+ @cache = {}
626
+ super(fields, profile)
627
+ end
628
+
66
629
  # Create a vCard 3.0 object with the minimum required fields, plus any
67
630
  # +fields+ you want in the card (they can also be added later).
68
631
  def Vcard.create(fields = [] )
@@ -139,23 +702,12 @@ module Vpim
139
702
  vcards
140
703
  end
141
704
 
142
- # The vCard version multiplied by 10 as an Integer. If no VERSION field
143
- # is present (which is non-conformant), nil is returned. For example, a
144
- # version 2.1 vCard would have a version of 21, and a version 3.0 vCard
145
- # would have a version of 30.
146
- def version
147
- v = self["version"]
148
- unless v
149
- raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
150
- end
151
- v = v.to_f * 10
152
- v = v.to_i
153
- end
154
-
155
705
  # The value of the field named +name+, optionally limited to fields of
156
706
  # type +type+. If no match is found, nil is returned, if multiple matches
157
707
  # are found, the first match to have one of its type values be 'PREF'
158
708
  # (preferred) is returned, otherwise the first match is returned.
709
+ #
710
+ # FIXME - this will become an alias for #value.
159
711
  def [](name, type=nil)
160
712
  fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
161
713
 
@@ -174,103 +726,675 @@ module Vpim
174
726
  fields.first ? fields.first.value : nil
175
727
  end
176
728
 
177
- # The name from a vCard, including all the components of the N: and FN:
178
- # fields.
729
+ # Return the Line#value for a specific +name+, and optionally for a
730
+ # specific +type+.
731
+ #
732
+ # If no line with the +name+ (and, optionally, +type+) exists, nil is
733
+ # returned.
734
+ #
735
+ # If multiple lines exist, the order of preference is:
736
+ # - lines with values over lines without
737
+ # - lines with a type of 'pref' over lines without
738
+ # If multiple lines are equally preferred, then the first line will be
739
+ # returned.
740
+ #
741
+ # This is most useful when looking for a line that can not occur multiple
742
+ # times, or when the line can occur multiple times, and you want to pick
743
+ # the first preferred line of a specific type. See #values if you need to
744
+ # access all the lines.
745
+ #
746
+ # Note that the +type+ field parameter is used for different purposes by
747
+ # the various kinds of vCard lines, but for the addressing lines (ADR,
748
+ # LABEL, TEL, EMAIL) it is has a reasonably consistent usage. Each
749
+ # addressing line can occur multiple times, and a +type+ of 'pref'
750
+ # indicates that a particular line is the preferred line. Other +type+
751
+ # values tend to indicate some information about the location ('home',
752
+ # 'work', ...) or some detail about the address ('cell', 'fax', 'voice',
753
+ # ...). See the methods for the specific types of line for information
754
+ # about supported types and their meaning.
755
+ def value(name, type = nil)
756
+ v = nil
179
757
 
180
- class Name
181
- # family name from N:
182
- attr_reader :family
183
- # given name from N:
184
- attr_reader :given
185
- # additional names from N:
186
- attr_reader :additional
187
- # such as "Ms." or "Dr.", from N:
188
- attr_reader :prefix
189
- # such as "BFA", from N:
190
- attr_reader :suffix
191
- # all the components of N: formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}"
192
- attr_reader :formatted
193
- # full name, the FN: field, a formatted version of the N: field, probably
194
- # in a form more align with the cultural conventions of the vCard owner
195
- # than +formatted+ is
196
- attr_reader :fullname
197
-
198
- def initialize(n, fn) #:nodoc:
199
- n = Vpim.decode_list(n, ';') do |item|
200
- item.strip
201
- end
758
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
202
759
 
203
- @family = n[0] || ""
204
- @given = n[1] || ""
205
- @additional = n[2] || ""
206
- @prefix = n[3] || ""
207
- @suffix = n[4] || ""
208
- @formatted = [ @prefix, @given, @additional, @family ].map{|i| i == '' ? nil : i}.compact.join(' ')
209
- if @suffix != ''
210
- @formatted << ', ' << @suffix
211
- end
760
+ valued = fields.select { |f| f.value != '' }
761
+ if valued.first
762
+ fields = valued
763
+ end
212
764
 
213
- # FIXME - make calls to #fullname fail if fn is nil
214
- @fullname = fn
765
+ pref = fields.select { |f| f.pref? }
766
+
767
+ if pref.first
768
+ fields = pref
769
+ end
770
+
771
+ if fields.first
772
+ line = Line.decode(@@decode, self, fields.first)
773
+
774
+ if line
775
+ return line.value
776
+ end
215
777
  end
216
778
 
779
+ nil
780
+ end
781
+
782
+ # A variant of #lines that only iterates over specific Line names. Since
783
+ # the name is known, only the Line#value is returned or yielded.
784
+ def values(name)
785
+ unless block_given?
786
+ lines(name).map { |line| line.value }
787
+ else
788
+ lines(name) { |line| yield line.value }
789
+ end
790
+ end
791
+
792
+ # The first ADR value of type +type+, a Address. Any of the location or
793
+ # delivery attributes of Address can be used as +type+. A wrapper around
794
+ # #value('ADR', +type+).
795
+ def address(type=nil)
796
+ value('ADR', type)
797
+ end
798
+
799
+ # The ADR values, an array of Address. If a block is given, the values are
800
+ # yielded. A wrapper around #values('ADR').
801
+ def addresses #:yield:address
802
+ values('ADR')
803
+ end
804
+
805
+ # The AGENT values. Each AGENT value is either a String, a Uri, or a Vcard.
806
+ # If a block is given, the values are yielded. A wrapper around
807
+ # #values('AGENT').
808
+ def agents #:yield:agent
809
+ values('AGENT')
810
+ end
811
+
812
+ # The BDAY value as either a Date or a DateTime, or nil if there is none.
813
+ #
814
+ # If the BDAY value is invalidly formatted, a feeble heuristic is applied
815
+ # to find the month and year, and return a Date in the current year.
816
+ def birthday
817
+ value('BDAY')
818
+ end
819
+
820
+ # The CATEGORIES values, an array of String. A wrapper around
821
+ # #value('CATEGORIES').
822
+ def categories
823
+ value('CATEGORIES')
824
+ end
825
+
826
+ # The first EMAIL value of type +type+, a Email. Any of the location
827
+ # attributes of Email can be used as +type+. A wrapper around
828
+ # #value('EMAIL', +type+).
829
+ def email(type=nil)
830
+ value('EMAIL', type)
831
+ end
832
+
833
+ # The EMAIL values, an array of Email. If a block is given, the values are
834
+ # yielded. A wrapper around #values('EMAIL').
835
+ def emails #:yield:email
836
+ values('EMAIL')
837
+ end
838
+
839
+ # The GEO value, an Array of two Floats, +[ latitude, longitude]+. North
840
+ # of the equator is positive latitude, east of the meridian is positive
841
+ # longitude. See RFC2445 for more info, there are lots of special cases
842
+ # and RFC2445's description is more complete thant RFC2426.
843
+ def geo
844
+ value('GEO')
845
+ end
846
+
847
+ # Return an Array of KEY Line#value, or yield each Line#value if a block
848
+ # is given. A wrapper around #values('KEY').
849
+ #
850
+ # KEY is a public key or authentication certificate associated with the
851
+ # object that the vCard represents. It is not commonly used, but could
852
+ # contain a X.509 or PGP certificate.
853
+ #
854
+ # See Attachment for a description of the value.
855
+ def keys(&proc) #:yield: Line.value
856
+ values('KEY', &proc)
857
+ end
858
+
859
+ # Return an Array of LOGO Line#value, or yield each Line#value if a block
860
+ # is given. A wrapper around #values('LOGO').
861
+ #
862
+ # LOGO is a graphic image of a logo associated with the object the vCard
863
+ # represents. Its not common, but would probably be equivalent to the logo
864
+ # on printed card.
865
+ #
866
+ # See Attachment for a description of the value.
867
+ def logos(&proc) #:yield: Line.value
868
+ values('LOGO', &proc)
217
869
  end
218
870
 
219
- # Returns the +name+ fields, N: and FN:, as a Name.
871
+ ## MAILER
872
+
873
+ # The N and FN as a Name object.
874
+ #
875
+ # N is required for a vCards, this raises InvalidEncodingError if
876
+ # there is no N so it cannot return nil.
220
877
  def name
221
- unless instance_variables.include? '@name'
222
- @name = Name.new(self['N'], self['FN'])
878
+ value('N') || raise(Vpim::InvalidEncodingError, "Missing mandatory N field")
879
+ end
880
+
881
+ # The first NICKNAME value, nil if there are none.
882
+ def nickname
883
+ v = value('NICKNAME')
884
+ v = v.first if v
885
+ v
886
+ end
887
+
888
+ # The NICKNAME values, an array of String. The array may be empty.
889
+ def nicknames
890
+ values('NICKNAME').flatten.uniq
891
+ end
892
+
893
+ # The NOTE value, a String. A wrapper around #value('NOTE').
894
+ def note
895
+ value('NOTE')
896
+ end
897
+
898
+ # The ORG value, an Array of String. The first string is the organization,
899
+ # subsequent strings are departments within the organization. A wrapper
900
+ # around #value('ORG').
901
+ def org
902
+ value('ORG')
903
+ end
904
+
905
+ # Return an Array of PHOTO Line#value, or yield each Line#value if a block
906
+ # is given. A wrapper around #values('PHOTO').
907
+ #
908
+ # PHOTO is an image or photograph information that annotates some aspect of
909
+ # the object the vCard represents. Commonly there is one PHOTO, and it is a
910
+ # photo of the person identified by the vCard.
911
+ #
912
+ # See Attachment for a description of the value.
913
+ def photos(&proc) #:yield: Line.value
914
+ values('PHOTO', &proc)
915
+ end
916
+
917
+ ## PRODID
918
+
919
+ ## PROFILE
920
+
921
+ ## REV
922
+
923
+ ## ROLE
924
+
925
+ # Return an Array of SOUND Line#value, or yield each Line#value if a block
926
+ # is given. A wrapper around #values('SOUND').
927
+ #
928
+ # SOUND is digital sound content information that annotates some aspect of
929
+ # the vCard. By default this type is used to specify the proper
930
+ # pronunciation of the name associated with the vCard. It is not commonly
931
+ # used. Also, note that there is no mechanism available to specify that the
932
+ # SOUND is being used for anything other than the default.
933
+ #
934
+ # See Attachment for a description of the value.
935
+ def sounds(&proc) #:yield: Line.value
936
+ values('SOUND', &proc)
937
+ end
938
+
939
+ ## SOURCE
940
+
941
+ # The first TEL value of type +type+, a Telephone. Any of the location or
942
+ # capability attributes of Telephone can be used as +type+. A wrapper around
943
+ # #value('TEL', +type+).
944
+ def telephone(type=nil)
945
+ value('TEL', type)
946
+ end
947
+
948
+ # The TEL values, an array of Telephone. If a block is given, the values are
949
+ # yielded. A wrapper around #values('TEL').
950
+ def telephones #:yield:tel
951
+ values('TEL')
952
+ end
953
+
954
+ ## TITLE
955
+
956
+ ## UID
957
+
958
+ # The URL value, a Uri. A wrapper around #value('NOTE').
959
+ def url
960
+ value('URL')
961
+ end
962
+
963
+ # The VERSION multiplied by 10 as an Integer. For example, a VERSION:2.1
964
+ # vCard would have a version of 21, and a VERSION:3.0 vCard would have a
965
+ # version of 30.
966
+ #
967
+ # VERSION is required for a vCard, this raises InvalidEncodingError if
968
+ # there is no VERSION so it cannot return nil.
969
+ def version
970
+ v = value('VERSION')
971
+ unless v
972
+ raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
223
973
  end
224
- @name
974
+ v
225
975
  end
226
976
 
227
- # Deprecated.
228
- def nickname #:nodoc:
229
- nn = self['NICKNAME']
230
- if nn && nn == ''
231
- nn = nil
977
+ # Make changes to a vCard.
978
+ #
979
+ # Yields a Vpim::Vcard::Maker that can be used to modify this vCard.
980
+ def make #:yield: maker
981
+ Vpim::Vcard::Maker.make2(self) do |maker|
982
+ yield maker
232
983
  end
233
- nn
234
984
  end
235
985
 
236
- # All the nicknames, [] if there are none.
237
- def nicknames
238
- enum_by_name('NICKNAME').select{|f| f.value != ''}.collect{|f| f.value}
986
+ # Delete +line+ if block yields true.
987
+ def delete_if #:nodoc: :yield: line
988
+ # Do in two steps to not mess up progress through the enumerator.
989
+ rm = []
990
+
991
+ each do |f|
992
+ line = f2l(f)
993
+ if line && yield(line)
994
+ rm << f
995
+
996
+ # Hack - because we treat N and FN as one field
997
+ if f.name? 'N'
998
+ rm << field('FN')
999
+ end
1000
+ end
1001
+ end
1002
+
1003
+ rm.each do |f|
1004
+ @fields.delete( f )
1005
+ @cache.delete( f )
1006
+ end
1007
+
239
1008
  end
240
1009
 
241
- # Returns the birthday as a Date on success, nil if there was no birthday
242
- # field, and raises an error if the birthday field could not be expressed
243
- # as a recurring event.
1010
+ # A class to make and make changes to vCards.
244
1011
  #
245
- # Also, I have a lot of vCards with dates that look like:
246
- # 678-09-23
247
- # The 678 is garbage, but 09-23 is really the birthday. This substitutes the
248
- # current year for the 3 digit year, and then converts to a Date.
249
- def birthday
250
- bday = self.field('BDAY')
1012
+ # It can be used to create completely new vCards using Vcard#make2.
1013
+ #
1014
+ # Its is also yielded from Vpim::Vcard#make, in which case it allows a kind
1015
+ # of transactional approach to changing vCards, so their values can be
1016
+ # validated after any changes have been made.
1017
+ #
1018
+ # Examples:
1019
+ # - link:ex_mkvcard.txt: example of creating a vCard
1020
+ # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
1021
+ # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
1022
+ # - link:ex_mkyourown.txt: example of adding support for new fields to Vcard::Maker
1023
+ class Maker
1024
+ # Make a vCard.
1025
+ #
1026
+ # Yields +maker+, a Vpim::Vcard::Maker which allows fields to be added to
1027
+ # +card+, and returns +card+, a Vpim::Vcard.
1028
+ #
1029
+ # If +card+ is nil or not provided a new Vpim::Vcard is created and the
1030
+ # fields are added to it.
1031
+ #
1032
+ # Defaults:
1033
+ # - vCards must have both an N and an FN field, #make2 will fail if there
1034
+ # is no N field in the +card+ when your block is finished adding fields.
1035
+ # - If there is an N field, but no FN field, FN will be set from the
1036
+ # information in N, see Vcard::Name#preformatted for more information.
1037
+ # - vCards must have a VERSION field. If one does not exist when your block is
1038
+ # is finished it will be set to 3.0.
1039
+ def self.make2(card = Vpim::Vcard.create, &block) # :yields: maker
1040
+ new(nil, card).make(&block)
1041
+ end
251
1042
 
252
- return nil unless bday
1043
+ # Deprecated, use #make2.
1044
+ #
1045
+ # If set, the FN field will be set to +full_name+. Otherwise, FN will
1046
+ # be set from the values in #name.
1047
+ def self.make(full_name = nil, &block) # :yields: maker
1048
+ new(full_name, Vpim::Vcard.create).make(&block)
1049
+ end
253
1050
 
254
- begin
255
- date = bday.to_date.first
1051
+ def make # :nodoc:
1052
+ yield self
1053
+ unless @card['N']
1054
+ raise Unencodeable, 'N field is mandatory'
1055
+ end
1056
+ fn = @card.field('FN')
1057
+ if fn && fn.value.strip.length == 0
1058
+ @card.delete(fn)
1059
+ fn = nil
1060
+ end
1061
+ unless fn
1062
+ @card << Vpim::DirectoryInfo::Field.create('FN', Vpim::Vcard::Name.new(@card['N'], '').formatted)
1063
+ end
1064
+ unless @card['VERSION']
1065
+ @card << Vpim::DirectoryInfo::Field.create('VERSION', "3.0")
1066
+ end
1067
+ @card
1068
+ end
256
1069
 
257
- rescue Vpim::InvalidEncodingError
258
- if bday.value =~ /(\d+)-(\d+)-(\d+)/
259
- y = $1.to_i
260
- m = $2.to_i
261
- d = $3.to_i
262
- if(y < 1900)
263
- y = Time.now.year
1070
+ private
1071
+
1072
+ def initialize(full_name, card) # :nodoc:
1073
+ @card = card || Vpim::Vcard::create
1074
+ if full_name
1075
+ @card << Vpim::DirectoryInfo::Field.create('FN', full_name.strip )
1076
+ end
1077
+ end
1078
+
1079
+ public
1080
+
1081
+ # def add_name # :yield: n
1082
+ # # FIXME: Each attribute can currently only have a single String value.
1083
+ # # FIXME: Need to escape specials in the String.
1084
+ # x = Struct.new(:family, :given, :additional, :prefix, :suffix).new
1085
+ # yield x
1086
+ # @card << Vpim::DirectoryInfo::Field.create(
1087
+ # 'N',
1088
+ # x.map { |s| s ? s.to_str : '' }
1089
+ # )
1090
+ # self
1091
+ # end
1092
+ # Set with #name now.
1093
+ # Use m.name do |n| n.fullname = foo end
1094
+ def fullname=(fullname) #:nodoc: bacwards compat
1095
+ if @card.field('FN')
1096
+ raise Vpim::InvalidEncodingError, "Not allowed to add more than one FN field to a vCard."
1097
+ end
1098
+ @card << Vpim::DirectoryInfo::Field.create( 'FN', fullname );
1099
+ end
1100
+
1101
+ # Set the name fields, N and FN.
1102
+ #
1103
+ # Attributes of +name+ are:
1104
+ # - family: family name
1105
+ # - given: given name
1106
+ # - additional: additional names
1107
+ # - prefix: such as "Ms." or "Dr."
1108
+ # - suffix: such as "BFA", or "Sensei"
1109
+ #
1110
+ # +name+ is a Vcard::Name.
1111
+ #
1112
+ # All attributes are optional, though have all names be zero-length
1113
+ # strings isn't really in the spirit of things. FN's value will be set
1114
+ # to Vcard::Name#formatted if Vcard::Name#fullname isn't given a specific
1115
+ # value.
1116
+ #
1117
+ # Warning: This is the only mandatory field.
1118
+ def name #:yield:name
1119
+ x = begin
1120
+ @card.name.dup
1121
+ rescue
1122
+ Vpim::Vcard::Name.new
1123
+ end
1124
+
1125
+ fn = x.fullname
1126
+
1127
+ yield x
1128
+
1129
+ x.fullname.strip!
1130
+
1131
+ delete_if do |line|
1132
+ line.name == 'N'
1133
+ end
1134
+
1135
+ @card << x.encode
1136
+ @card << x.encode_fn
1137
+
1138
+ self
1139
+ end
1140
+
1141
+ alias :add_name :name #:nodoc: backwards compatibility
1142
+
1143
+ # Add an address field, ADR. +address+ is a Vpim::Vcard::Address.
1144
+ def add_addr # :yield: address
1145
+ x = Vpim::Vcard::Address.new
1146
+ yield x
1147
+ @card << x.encode
1148
+ self
1149
+ end
1150
+
1151
+ # Add a telephone field, TEL. +tel+ is a Vpim::Vcard::Telephone.
1152
+ #
1153
+ # The block is optional, its only necessary if you want to specify
1154
+ # the optional attributes.
1155
+ def add_tel(number) # :yield: tel
1156
+ x = Vpim::Vcard::Telephone.new(number)
1157
+ if block_given?
1158
+ yield x
1159
+ end
1160
+ @card << x.encode
1161
+ self
1162
+ end
1163
+
1164
+ # Add an email field, EMAIL. +email+ is a Vpim::Vcard::Email.
1165
+ #
1166
+ # The block is optional, its only necessary if you want to specify
1167
+ # the optional attributes.
1168
+ def add_email(email) # :yield: email
1169
+ x = Vpim::Vcard::Email.new(email)
1170
+ if block_given?
1171
+ yield x
1172
+ end
1173
+ @card << x.encode
1174
+ self
1175
+ end
1176
+
1177
+ # Set the nickname field, NICKNAME.
1178
+ #
1179
+ # It can be set to a single String or an Array of String.
1180
+ def nickname=(nickname)
1181
+ delete_if { |l| l.name == 'NICKNAME' }
1182
+
1183
+ @card << Vpim::DirectoryInfo::Field.create( 'NICKNAME', nickname );
1184
+ end
1185
+
1186
+ # Add a birthday field, BDAY.
1187
+ #
1188
+ # +birthday+ must be a time or date object.
1189
+ #
1190
+ # Warning: It may confuse both humans and software if you add multiple
1191
+ # birthdays.
1192
+ def birthday=(birthday)
1193
+ if !birthday.respond_to? :month
1194
+ raise ArgumentError, 'birthday must be a date or time object.'
1195
+ end
1196
+ delete_if { |l| l.name == 'BDAY' }
1197
+ @card << Vpim::DirectoryInfo::Field.create( 'BDAY', birthday );
1198
+ end
1199
+
1200
+ # Add a note field, NOTE. The +note+ String can contain newlines, they
1201
+ # will be escaped.
1202
+ def add_note(note)
1203
+ @card << Vpim::DirectoryInfo::Field.create( 'NOTE', Vpim.encode_text(note) );
1204
+ end
1205
+
1206
+ # Add an instant-messaging/point of presence address field, IMPP. The address
1207
+ # is a URL, with the syntax depending on the protocol.
1208
+ #
1209
+ # Attributes of IMPP are:
1210
+ # - preferred: true - set if this is the preferred address
1211
+ # - location: home, work, mobile - location of address
1212
+ # - purpose: personal,business - purpose of communications
1213
+ #
1214
+ # All attributes are optional, and so is the block.
1215
+ #
1216
+ # The URL syntaxes for the messaging schemes is fairly complicated, so I
1217
+ # don't try and build the URLs here, maybe in the future. This forces
1218
+ # the user to know the URL for their own address, hopefully not too much
1219
+ # of a burden.
1220
+ #
1221
+ # IMPP is defined in draft-jennings-impp-vcard-04.txt. It refers to the
1222
+ # URI scheme of a number of messaging protocols, but doesn't give
1223
+ # references to all of them:
1224
+ # - "xmpp" indicates to use XMPP, draft-saintandre-xmpp-uri-06.txt
1225
+ # - "irc" or "ircs" indicates to use IRC, draft-butcher-irc-url-04.txt
1226
+ # - "sip" indicates to use SIP/SIMPLE, RFC 3261
1227
+ # - "im" or "pres" indicates to use a CPIM or CPP gateway, RFC 3860 and RFC 3859
1228
+ # - "ymsgr" indicates to use yahoo
1229
+ # - "msn" might indicate to use Microsoft messenger
1230
+ # - "aim" indicates to use AOL
1231
+ #
1232
+ def add_impp(url) # :yield: impp
1233
+ params = {}
1234
+
1235
+ if block_given?
1236
+ x = Struct.new( :location, :preferred, :purpose ).new
1237
+
1238
+ yield x
1239
+
1240
+ x[:preferred] = 'PREF' if x[:preferred]
1241
+
1242
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1243
+
1244
+ params['TYPE'] = types if types.first
1245
+ end
1246
+
1247
+ @card << Vpim::DirectoryInfo::Field.create( 'IMPP', url, params)
1248
+ self
1249
+ end
1250
+
1251
+ # Add an X-AIM account name where +xaim+ is an AIM screen name.
1252
+ #
1253
+ # I don't know if this is conventional, or supported by anything other
1254
+ # than AddressBook.app, but an example is:
1255
+ # X-AIM;type=HOME;type=pref:exampleaccount
1256
+ #
1257
+ # Attributes of X-AIM are:
1258
+ # - preferred: true - set if this is the preferred address
1259
+ # - location: home, work, mobile - location of address
1260
+ #
1261
+ # All attributes are optional, and so is the block.
1262
+ def add_x_aim(xaim) # :yield: xaim
1263
+ params = {}
1264
+
1265
+ if block_given?
1266
+ x = Struct.new( :location, :preferred ).new
1267
+
1268
+ yield x
1269
+
1270
+ x[:preferred] = 'PREF' if x[:preferred]
1271
+
1272
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1273
+
1274
+ params['TYPE'] = types if types.first
1275
+ end
1276
+
1277
+ @card << Vpim::DirectoryInfo::Field.create( 'X-AIM', xaim, params)
1278
+ self
1279
+ end
1280
+
1281
+
1282
+ # Add a photo field, PHOTO.
1283
+ #
1284
+ # Attributes of PHOTO are:
1285
+ # - image: set to image data to include inline
1286
+ # - link: set to the URL of the image data
1287
+ # - type: string identifying the image type, supposed to be an "IANA registered image format",
1288
+ # or a non-registered image format (usually these start with an x-)
1289
+ #
1290
+ # An error will be raised if neither image or link is set, or if both image
1291
+ # and link is set.
1292
+ #
1293
+ # Setting type is optional for a link image, because either the URL, the
1294
+ # image file extension, or a HTTP Content-Type may specify the type. If
1295
+ # it's not a link, setting type is mandatory, though it can be set to an
1296
+ # empty string, <code>''</code>, if the type is unknown.
1297
+ #
1298
+ # TODO - I'm not sure about this API. I'm thinking maybe it should be
1299
+ # #add_photo(image, type), and that I should detect when the image is a
1300
+ # URL, and make type mandatory if it wasn't a URL.
1301
+ def add_photo # :yield: photo
1302
+ x = Struct.new(:image, :link, :type).new
1303
+ yield x
1304
+ if x[:image] && x[:link]
1305
+ raise Vpim::InvalidEncodingError, 'Image is not allowed to be both inline and a link.'
1306
+ end
1307
+
1308
+ value = x[:image] || x[:link]
1309
+
1310
+ if !value
1311
+ raise Vpim::InvalidEncodingError, 'A image link or inline data must be provided.'
1312
+ end
1313
+
1314
+ params = {}
1315
+
1316
+ # Don't set type to the empty string.
1317
+ params['TYPE'] = x[:type] if( x[:type] && x[:type].length > 0 )
1318
+
1319
+ if x[:link]
1320
+ params['VALUE'] = 'URI'
1321
+ else # it's inline, base-64 encode it
1322
+ params['ENCODING'] = :b64
1323
+ if !x[:type]
1324
+ raise Vpim::InvalidEncodingError, 'Inline image data must have it\'s type set.'
264
1325
  end
265
- date = Date.new(y, m, d)
1326
+ end
1327
+
1328
+ @card << Vpim::DirectoryInfo::Field.create( 'PHOTO', value, params )
1329
+ self
1330
+ end
1331
+
1332
+ # Add a URL field, URL.
1333
+ def add_url(url)
1334
+ @card << Vpim::DirectoryInfo::Field.create( 'URL', url.to_str );
1335
+ end
1336
+
1337
+ # Add a Field, +field+.
1338
+ def add_field(field)
1339
+ fieldname = field.name.upcase
1340
+ case
1341
+ when [ 'BEGIN', 'END' ].include?(fieldname)
1342
+ raise Vpim::InvalidEncodingError, "Not allowed to manually add #{field.name} to a vCard."
1343
+
1344
+ when [ 'VERSION', 'N', 'FN' ].include?(fieldname)
1345
+ if @card.field(fieldname)
1346
+ raise Vpim::InvalidEncodingError, "Not allowed to add more than one #{fieldname} to a vCard."
1347
+ end
1348
+ @card << field
1349
+
266
1350
  else
267
- raise
1351
+ @card << field
268
1352
  end
269
1353
  end
270
1354
 
271
- date
272
- end
1355
+ # Copy the fields from +card+ into self using #add_field. If a block is
1356
+ # provided, each Field from +card+ is yielded. The block should return a
1357
+ # Field to add, or nil. The Field doesn't have to be the one yielded,
1358
+ # allowing the field to be copied and modified (see Field#copy) before adding, or
1359
+ # not added at all if the block yields nil.
1360
+ #
1361
+ # The vCard fields BEGIN and END aren't copied, and VERSION, N, and FN are copied
1362
+ # only if the card doesn't have them already.
1363
+ def copy(card) # :yields: Field
1364
+ card.each do |field|
1365
+ fieldname = field.name.upcase
1366
+ case
1367
+ when [ 'BEGIN', 'END' ].include?(fieldname)
1368
+ # Never copy these
1369
+
1370
+ when [ 'VERSION', 'N', 'FN' ].include?(fieldname) && @card.field(fieldname)
1371
+ # Copy these only if they don't already exist.
1372
+
1373
+ else
1374
+ if block_given?
1375
+ field = yield field
1376
+ end
1377
+
1378
+ if field
1379
+ add_field(field)
1380
+ end
1381
+ end
1382
+ end
1383
+ end
1384
+
1385
+ # Delete +line+ if block yields true.
1386
+ def delete_if #:yield: line
1387
+ begin
1388
+ @card.delete_if do |line|
1389
+ yield line
1390
+ end
1391
+ rescue NoMethodError
1392
+ # FIXME - this is a hideous hack, allowing a DirectoryInfo to
1393
+ # be passed instead of a Vcard, and for it to almost work. Yuck.
1394
+ end
1395
+ end
273
1396
 
1397
+ end
274
1398
  end
275
1399
  end
276
1400