vpim 0.16

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.
@@ -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
+