vpim 0.16

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