vcard 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,30 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Vpim
10
+ # This is a way for an object to have multiple ways of being enumerated via
11
+ # argument to it's #each() method. An Enumerator mixes in Enumerable, so the
12
+ # standard APIs such as Enumerable#map(), Enumerable#to_a(), and
13
+ # Enumerable#find_all() can be used on it.
14
+ #
15
+ # TODO since 1.8, this is part of the standard library, I should rewrite vPim
16
+ # so this can be removed.
17
+ class Enumerator
18
+ include Enumerable
19
+
20
+ def initialize(obj, *args)
21
+ @obj = obj
22
+ @args = args
23
+ end
24
+
25
+ def each(&block)
26
+ @obj.each(*@args, &block)
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,610 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Vpim
10
+
11
+ class DirectoryInfo
12
+
13
+ # A field in a directory info object.
14
+ class Field
15
+ # TODO
16
+ # - Field should know which param values and field values are
17
+ # case-insensitive, configurably, so it can down case them
18
+ # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or
19
+ # pvalue_iset/idel/iadd, where set sets them all, add adds if not present,
20
+ # and del deletes any that are present
21
+ # - I really, really, need a case-insensitive string...
22
+ # - should allow nil as a field value, its not the same as '', if there is
23
+ # more than one pvalue, the empty string will show up. This isn't strictly
24
+ # disallowed, but its odd. Should also strip empty strings on decoding, if
25
+ # I don't already.
26
+ private_class_method :new
27
+
28
+ def Field.create_array(fields)
29
+ case fields
30
+ when Hash
31
+ fields.map do |name,value|
32
+ DirectoryInfo::Field.create( name, value )
33
+ end
34
+ else
35
+ fields.to_ary
36
+ end
37
+ end
38
+
39
+ # Encode a field.
40
+ def Field.encode0(group, name, params={}, value='') # :nodoc:
41
+ line = ""
42
+
43
+ # A reminder of the line format:
44
+ # [<group>.]<name>;<pname>=<pvalue>,<pvalue>:<value>
45
+
46
+ if group
47
+ line << group << '.'
48
+ end
49
+
50
+ line << name
51
+
52
+ params.each do |pname, pvalues|
53
+
54
+ unless pvalues.respond_to? :to_ary
55
+ pvalues = [ pvalues ]
56
+ end
57
+
58
+ line << ';' << pname << '='
59
+
60
+ sep = "" # set to ',' after one pvalue has been appended
61
+
62
+ pvalues.each do |pvalue|
63
+ # check if we need to do any encoding
64
+ if pname.casecmp('ENCODING') == 0 && pvalue == :b64
65
+ pvalue = 'B' # the RFC definition of the base64 param value
66
+ value = [ value.to_str ].pack('m').gsub("\n", '')
67
+ end
68
+
69
+ line << sep << pvalue
70
+ sep =",";
71
+ end
72
+ end
73
+
74
+ line << ':'
75
+
76
+ line << Field.value_str(value)
77
+
78
+ line
79
+ end
80
+
81
+ def Field.value_str(value) # :nodoc:
82
+ line = ''
83
+ case value
84
+ when Date
85
+ line << Vpim.encode_date(value)
86
+
87
+ when Time #, DateTime
88
+ line << Vpim.encode_date_time(value)
89
+
90
+ when Array
91
+ line << value.map { |v| Field.value_str(v) }.join(';')
92
+
93
+ when Symbol
94
+ line << value
95
+
96
+ else
97
+ # FIXME - somewhere along here, values with special chars need escaping...
98
+ line << value.to_str
99
+ end
100
+ line
101
+ end
102
+
103
+ # Decode a field.
104
+ def Field.decode0(atline) # :nodoc:
105
+ unless atline =~ %r{#{Bnf::LINE}}i
106
+ raise Vpim::InvalidEncodingError, atline
107
+ end
108
+
109
+ atgroup = $1.upcase
110
+ atname = $2.upcase
111
+ paramslist = $3
112
+ atvalue = $~[-1]
113
+
114
+ # I've seen space that shouldn't be there, as in "BEGIN:VCARD ", so
115
+ # strip it. I'm not absolutely sure this is allowed... it certainly
116
+ # breaks round-trip encoding.
117
+ atvalue.strip!
118
+
119
+ if atgroup.length > 0
120
+ atgroup.chomp!('.')
121
+ else
122
+ atgroup = nil
123
+ end
124
+
125
+ atparams = {}
126
+
127
+ # Collect the params, if any.
128
+ if paramslist.size > 1
129
+
130
+ # v3.0 and v2.1 params
131
+ paramslist.scan( %r{#{Bnf::PARAM}}i ) do
132
+
133
+ # param names are case-insensitive, and multi-valued
134
+ name = $1.upcase
135
+ params = $3
136
+
137
+ # v2.1 params have no '=' sign, figure out what kind of param it
138
+ # is (either its a known encoding, or we treat it as a 'TYPE'
139
+ # param).
140
+
141
+ if $2 == ""
142
+ params = $1
143
+ case $1
144
+ when /quoted-printable/i
145
+ name = 'ENCODING'
146
+
147
+ when /base64/i
148
+ name = 'ENCODING'
149
+
150
+ else
151
+ name = 'TYPE'
152
+ end
153
+ end
154
+
155
+ # TODO - In ruby1.8 I can give an initial value to the atparams
156
+ # hash values instead of this.
157
+ unless atparams.key? name
158
+ atparams[name] = []
159
+ end
160
+
161
+ params.scan( %r{#{Bnf::PVALUE}} ) do
162
+ atparams[name] << ($1 || $2)
163
+ end
164
+ end
165
+ end
166
+
167
+ [ atgroup, atname, atparams, atvalue ]
168
+ end
169
+
170
+ def initialize(line) # :nodoc:
171
+ @line = line.to_str
172
+ @group, @name, @params, @value = Field.decode0(@line)
173
+
174
+ @params.each do |pname,pvalues|
175
+ pvalues.freeze
176
+ end
177
+ self
178
+ end
179
+
180
+ # Create a field by decoding +line+, a String which must already be
181
+ # unfolded. Decoded fields are frozen, but see #copy().
182
+ def Field.decode(line)
183
+ new(line).freeze
184
+ end
185
+
186
+ # Create a field with name +name+ (a String), value +value+ (see below),
187
+ # and optional parameters, +params+. +params+ is a hash of the parameter
188
+ # name (a String) to either a single string or symbol, or an array of
189
+ # strings and symbols (parameters can be multi-valued).
190
+ #
191
+ # If 'ENCODING' => :b64 is specified as a parameter, the value will be
192
+ # base-64 encoded. If it's already base-64 encoded, then use String
193
+ # values ('ENCODING' => 'B'), and no further encoding will be done by
194
+ # this routine.
195
+ #
196
+ # Currently handled value types are:
197
+ # - Time, encoded as a date-time value
198
+ # - Date, encoded as a date value
199
+ # - String, encoded directly
200
+ # - Array of String, concatentated with ';' between them.
201
+ #
202
+ # TODO - need a way to encode String values as TEXT, at least optionally,
203
+ # so as to escape special chars, etc.
204
+ def Field.create(name, value="", params={})
205
+ line = Field.encode0(nil, name, params, value)
206
+
207
+ begin
208
+ new(line)
209
+ rescue Vpim::InvalidEncodingError => e
210
+ raise ArgumentError, e.to_s
211
+ end
212
+ end
213
+
214
+ # Create a copy of Field. If the original Field was frozen, this one
215
+ # won't be.
216
+ def copy
217
+ Marshal.load(Marshal.dump(self))
218
+ end
219
+
220
+ # The String encoding of the Field. The String will be wrapped to a
221
+ # maximum line width of +width+, where +0+ means no wrapping, and nil is
222
+ # to accept the default wrapping (75, recommended by RFC2425).
223
+ #
224
+ # Note: AddressBook.app 3.0.3 neither understands to unwrap lines when it
225
+ # imports vCards (it treats them as raw new-line characters), nor wraps
226
+ # long lines on export. This is mostly a cosmetic problem, but wrapping
227
+ # can be disabled by setting width to +0+, if desired.
228
+ #
229
+ # FIXME - breaks round-trip encoding, need to change this to not wrap
230
+ # fields that are already wrapped.
231
+ def encode(width=nil)
232
+ width = 75 unless width
233
+ l = @line
234
+ # Wrap to width, unless width is zero.
235
+ if width > 0
236
+ l = l.gsub(/.{#{width},#{width}}/) { |m| m + "\n " }
237
+ end
238
+ # Make sure it's terminated with no more than a single NL.
239
+ l.gsub(/\s*\z/, '') + "\n"
240
+ end
241
+
242
+ alias to_s encode
243
+
244
+ # The name.
245
+ def name
246
+ @name
247
+ end
248
+
249
+ # The group, if present, or nil if not present.
250
+ def group
251
+ @group
252
+ end
253
+
254
+ # An Array of all the param names.
255
+ def pnames
256
+ @params.keys
257
+ end
258
+
259
+ # FIXME - remove my own uses of #params
260
+ alias params pnames # :nodoc:
261
+
262
+ # The first value of the param +name+, nil if there is no such param,
263
+ # the param has no value, or the first param value is zero-length.
264
+ def pvalue(name)
265
+ v = pvalues( name )
266
+ if v
267
+ v = v.first
268
+ end
269
+ if v
270
+ v = nil unless v.length > 0
271
+ end
272
+ v
273
+ end
274
+
275
+ # The Array of all values of the param +name+, nil if there is no such
276
+ # param, [] if the param has no values. If the Field isn't frozen, the
277
+ # Array is mutable.
278
+ def pvalues(name)
279
+ @params[name.upcase]
280
+ end
281
+
282
+ # FIXME - remove my own uses of #param
283
+ alias param pvalues # :nodoc:
284
+
285
+ alias [] pvalues
286
+
287
+ # Yield once for each param, +name+ is the parameter name, +value+ is an
288
+ # array of the parameter values.
289
+ def each_param(&block) #:yield: name, value
290
+ if @params
291
+ @params.each(&block)
292
+ end
293
+ end
294
+
295
+ # The decoded value.
296
+ #
297
+ # The encoding specified by the #encoding, if any, is stripped.
298
+ #
299
+ # Note: Both the RFC 2425 encoding param ("b", meaning base-64) and the
300
+ # vCard 2.1 encoding params ("base64", "quoted-printable", "8bit", and
301
+ # "7bit") are supported.
302
+ #
303
+ # FIXME:
304
+ # - should use the VALUE parameter
305
+ # - should also take a default value type, so it can be converted
306
+ # if VALUE parameter is not present.
307
+ def value
308
+ case encoding
309
+ when nil, '8BIT', '7BIT' then @value
310
+
311
+ # Hack - if the base64 lines started with 2 SPC chars, which is invalid,
312
+ # there will be extra spaces in @value. Since no SPC chars show up in
313
+ # b64 encodings, they can be safely stripped out before unpacking.
314
+ when 'B', 'BASE64' then @value.gsub(' ', '').unpack('m*').first
315
+
316
+ when 'QUOTED-PRINTABLE' then @value.unpack('M*').first
317
+
318
+ else
319
+ raise Vpim::InvalidEncodingError, "unrecognized encoding (#{encoding})"
320
+ end
321
+ end
322
+
323
+ # Is the #name of this Field +name+? Names are case insensitive.
324
+ def name?(name)
325
+ @name.casecmp(name) == 0
326
+ end
327
+
328
+ # Is the #group of this field +group+? Group names are case insensitive.
329
+ # A +group+ of nil matches if the field has no group.
330
+ def group?(group)
331
+ @group.casecmp(group) == 0
332
+ end
333
+
334
+ # Is the value of this field of type +kind+? RFC2425 allows the type of
335
+ # a fields value to be encoded in the VALUE parameter. Don't rely on its
336
+ # presence, they aren't required, and usually aren't bothered with. In
337
+ # cases where the kind of value might vary (an iCalendar DTSTART can be
338
+ # either a date or a date-time, for example), you are more likely to see
339
+ # the kind of value specified explicitly.
340
+ #
341
+ # The value types defined by RFC 2425 are:
342
+ # - uri:
343
+ # - text:
344
+ # - date: a list of 1 or more dates
345
+ # - time: a list of 1 or more times
346
+ # - date-time: a list of 1 or more date-times
347
+ # - integer:
348
+ # - boolean:
349
+ # - float:
350
+ def kind?(kind)
351
+ self.kind.casecmp(kind) == 0
352
+ end
353
+
354
+ # Is one of the values of the TYPE parameter of this field +type+? The
355
+ # type parameter values are case insensitive. False if there is no TYPE
356
+ # parameter.
357
+ #
358
+ # TYPE parameters are used for general categories, such as
359
+ # distinguishing between an email address used at home or at work.
360
+ def type?(type)
361
+ type = type.to_str
362
+
363
+ types = param('TYPE')
364
+
365
+ if types
366
+ types = types.detect { |t| t.casecmp(type) == 0 }
367
+ end
368
+ end
369
+
370
+ # Is this field marked as preferred? A vCard field is preferred if
371
+ # #type?('PREF'). This method is not necessarily meaningful for
372
+ # non-vCard profiles.
373
+ def pref?
374
+ type? 'PREF'
375
+ end
376
+
377
+ # Set whether a field is marked as preferred. See #pref?
378
+ def pref=(ispref)
379
+ if ispref
380
+ pvalue_iadd('TYPE', 'PREF')
381
+ else
382
+ pvalue_idel('TYPE', 'PREF')
383
+ end
384
+ end
385
+
386
+ # Is the value of this field +value+? The check is case insensitive.
387
+ # FIXME - it shouldn't be insensitive, make a #casevalue? method.
388
+ def value?(value)
389
+ @value.casecmp(value) == 0
390
+ end
391
+
392
+ # The value of the ENCODING parameter, if present, or nil if not
393
+ # present.
394
+ def encoding
395
+ e = param('ENCODING')
396
+
397
+ if e
398
+ if e.length > 1
399
+ raise Vpim::InvalidEncodingError, "multi-valued param 'ENCODING' (#{e})"
400
+ end
401
+ e = e.first.upcase
402
+ end
403
+ e
404
+ end
405
+
406
+ # The type of the value, as specified by the VALUE parameter, nil if
407
+ # unspecified.
408
+ def kind
409
+ v = param('VALUE')
410
+ if v
411
+ if v.size > 1
412
+ raise InvalidEncodingError, "multi-valued param 'VALUE' (#{values})"
413
+ end
414
+ v = v.first.downcase
415
+ end
416
+ v
417
+ end
418
+
419
+ # The value as an array of Time objects (all times and dates in
420
+ # RFC2425 are lists, even where it might not make sense, such as a
421
+ # birthday). The time will be UTC if marked as so (with a timezone of
422
+ # "Z"), and in localtime otherwise.
423
+ #
424
+ # TODO - support timezone offsets
425
+ #
426
+ # TODO - if year is before 1970, this won't work... but some people
427
+ # are generating calendars saying Canada Day started in 1753!
428
+ # That's just wrong! So, what to do? I add a message
429
+ # saying what the year is that breaks, so they at least know that
430
+ # its ridiculous! I think I need my own DateTime variant.
431
+ def to_time
432
+ begin
433
+ Vpim.decode_date_time_list(value).collect do |d|
434
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
435
+ begin
436
+ if(d.pop == "Z")
437
+ Time.gm(*d)
438
+ else
439
+ Time.local(*d)
440
+ end
441
+ rescue ArgumentError => e
442
+ raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
443
+ end
444
+ end
445
+ rescue Vpim::InvalidEncodingError
446
+ Vpim.decode_date_list(value).collect do |d|
447
+ # We get [ year, month, day ]
448
+ begin
449
+ Time.gm(*d)
450
+ rescue ArgumentError => e
451
+ raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
452
+ end
453
+ end
454
+ end
455
+ end
456
+
457
+ # The value as an array of Date objects (all times and dates in
458
+ # RFC2425 are lists, even where it might not make sense, such as a
459
+ # birthday).
460
+ #
461
+ # The field value may be a list of either DATE or DATE-TIME values,
462
+ # decoding is tried first as a DATE-TIME, then as a DATE, if neither
463
+ # works an InvalidEncodingError will be raised.
464
+ def to_date
465
+ begin
466
+ Vpim.decode_date_time_list(value).collect do |d|
467
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
468
+ Date.new(d[0], d[1], d[2])
469
+ end
470
+ rescue Vpim::InvalidEncodingError
471
+ Vpim.decode_date_list(value).collect do |d|
472
+ # We get [ year, month, day ]
473
+ Date.new(*d)
474
+ end
475
+ end
476
+ end
477
+
478
+ # The value as text. Text can have escaped newlines, commas, and escape
479
+ # characters, this method will strip them, if present.
480
+ #
481
+ # In theory, #value could also do this, but it would need to know that
482
+ # the value is of type 'TEXT', and often for text values the 'VALUE'
483
+ # parameter is not present, so knowledge of the expected type of the
484
+ # field is required from the decoder.
485
+ def to_text
486
+ Vpim.decode_text(value)
487
+ end
488
+
489
+ # The undecoded value, see +value+.
490
+ def value_raw
491
+ @value
492
+ end
493
+
494
+ # TODO def pretty_print() ...
495
+
496
+ # Set the group of this field to +group+.
497
+ def group=(group)
498
+ mutate(group, @name, @params, @value)
499
+ group
500
+ end
501
+
502
+ # Set the value of this field to +value+. Valid values are as in
503
+ # Field.create().
504
+ def value=(value)
505
+ mutate(@group, @name, @params, value)
506
+ value
507
+ end
508
+
509
+ # Convert +value+ to text, then assign.
510
+ #
511
+ # TODO - unimplemented
512
+ def text=(text)
513
+ end
514
+
515
+ # Set a the param +pname+'s value to +pvalue+, replacing any value it
516
+ # currently has. See Field.create() for a description of +pvalue+.
517
+ #
518
+ # Example:
519
+ # if field['TYPE']
520
+ # field['TYPE'] << 'HOME'
521
+ # else
522
+ # field['TYPE'] = [ 'HOME' ]
523
+ # end
524
+ #
525
+ # TODO - this could be an alias to #pvalue_set
526
+ def []=(pname,pvalue)
527
+ unless pvalue.respond_to?(:to_ary)
528
+ pvalue = [ pvalue ]
529
+ end
530
+
531
+ h = @params.dup
532
+
533
+ h[pname.upcase] = pvalue
534
+
535
+ mutate(@group, @name, h, @value)
536
+ pvalue
537
+ end
538
+
539
+ # Add +pvalue+ to the param +pname+'s value. The values are treated as a
540
+ # set so duplicate values won't occur, and String values are case
541
+ # insensitive. See Field.create() for a description of +pvalue+.
542
+ def pvalue_iadd(pname, pvalue)
543
+ pname = pname.upcase
544
+
545
+ # Get a uniq set, where strings are compared case-insensitively.
546
+ values = [ pvalue, @params[pname] ].flatten.compact
547
+ values = values.collect do |v|
548
+ if v.respond_to? :to_str
549
+ v = v.to_str.upcase
550
+ end
551
+ v
552
+ end
553
+ values.uniq!
554
+
555
+ h = @params.dup
556
+
557
+ h[pname] = values
558
+
559
+ mutate(@group, @name, h, @value)
560
+ values
561
+ end
562
+
563
+ # Delete +pvalue+ from the param +pname+'s value. The values are treated
564
+ # as a set so duplicate values won't occur, and String values are case
565
+ # insensitive. +pvalue+ must be a single String or Symbol.
566
+ def pvalue_idel(pname, pvalue)
567
+ pname = pname.upcase
568
+ if pvalue.respond_to? :to_str
569
+ pvalue = pvalue.to_str.downcase
570
+ end
571
+
572
+ # Get a uniq set, where strings are compared case-insensitively.
573
+ values = [ nil, @params[pname] ].flatten.compact
574
+ values = values.collect do |v|
575
+ if v.respond_to? :to_str
576
+ v = v.to_str.downcase
577
+ end
578
+ v
579
+ end
580
+ values.uniq!
581
+ values.delete pvalue
582
+
583
+ h = @params.dup
584
+
585
+ h[pname] = values
586
+
587
+ mutate(@group, @name, h, @value)
588
+ values
589
+ end
590
+
591
+ # FIXME - should change this so it doesn't assign to @line here, so @line
592
+ # is used to preserve original encoding. That way, #encode can only wrap
593
+ # new fields, not old fields.
594
+ def mutate(g, n, p, v) #:nodoc:
595
+ line = Field.encode0(g, n, p, v)
596
+
597
+ begin
598
+ @group, @name, @params, @value = Field.decode0(line)
599
+ @line = line
600
+ rescue Vpim::InvalidEncodingError => e
601
+ raise ArgumentError, e.to_s
602
+ end
603
+ self
604
+ end
605
+
606
+ private :mutate
607
+ end
608
+ end
609
+ end
610
+