vpim 0.323 → 0.357

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