vpim 0.323 → 0.357

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,9 +6,8 @@
6
6
  details.
7
7
  =end
8
8
 
9
- # I think a file like this may be required by ruby-gems.
9
+ # the existence of this file is a hack to support users or rubygems
10
10
 
11
11
  require 'vpim/icalendar'
12
12
  require 'vpim/vcard'
13
- require 'vpim/maker/vcard'
14
13
 
@@ -0,0 +1,181 @@
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
+ =begin
10
+
11
+ Notes on a CAL-ADDRESS
12
+
13
+ When used with ATTENDEE, the parameters are:
14
+ CN
15
+ CUTYPE
16
+ DELEGATED-FROM
17
+ DELEGATED-TO
18
+ DIR
19
+ LANGUAGE
20
+ MEMBER
21
+ PARTSTAT
22
+ ROLE
23
+ RSVP
24
+ SENT-BY
25
+
26
+ When used with ORGANIZER, the parameters are:
27
+ CN
28
+ DIR
29
+ LANGUAGE
30
+ SENT-BY
31
+
32
+
33
+ What I've seen in Notes invitations, and iCal responses:
34
+ ROLE
35
+ PARTSTAT
36
+ RSVP
37
+ CN
38
+
39
+ Support these last 4, for now.
40
+
41
+ =end
42
+
43
+ module Vpim
44
+ class Icalendar
45
+ # Used to represent calendar fields containing CAL-ADDRESS values.
46
+ # The organizer or the attendees of a calendar event are examples of such
47
+ # a field.
48
+ #
49
+ # Example:
50
+ # ORGANIZER;CN="A. Person":mailto:a_person@example.com
51
+ # ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION
52
+ # ;CN="Sam Roberts";RSVP=TRUE:mailto:SRoberts@example.com
53
+ #
54
+ class Address
55
+
56
+ # Create an Address from a DirectoryInfo::Field, +field+.
57
+ #
58
+ # TODO - make private, and split into the encode/decode/create trinity.
59
+ def initialize(field)
60
+ unless field.value
61
+ raise ArgumentError
62
+ end
63
+
64
+ @field = field
65
+ end
66
+
67
+ # Return a representation of this Address as a DirectoryInfo::Field.
68
+ def field
69
+ @field.copy
70
+ end
71
+
72
+ # Create a copy of Address. If the original Address was frozen, this one
73
+ # won't be.
74
+ def copy
75
+ Marshal.load(Marshal.dump(self))
76
+ end
77
+
78
+ # Addresses in a CAL-ADDRESS are represented as a URI, usually a mailto URI.
79
+ def uri
80
+ @field.value
81
+ end
82
+
83
+ # Return true if the +uri+ is == to this address' URI. The comparison
84
+ # is case-insensitive.
85
+ #
86
+ # FIXME - why case insensitive? Email addresses. Should use a URI library
87
+ # if I can find one and it knows how to do URI comparisons.
88
+ def ==(uri)
89
+ Vpim::Methods.casecmp?(self.uri.to_str, uri.to_str)
90
+ end
91
+
92
+ # The common or displayable name associated with the calendar address,
93
+ # or nil if there is none.
94
+ def cn
95
+ return nil unless n = @field.param('CN')
96
+
97
+ # FIXME = the CN param may have no value, which is an error, but don't try
98
+ # to decode it, return either nil, or InvalidEncoding
99
+ Vpim.decode_text(n.first)
100
+ end
101
+
102
+ # A string representation of an address, using the common name, and the
103
+ # URI. The URI protocol is stripped if it's "mailto:".
104
+ #
105
+ # TODO - this needs to properly escape the cn string!
106
+ def to_s
107
+ u = uri
108
+ u.gsub!(/^mailto: */i, '')
109
+
110
+ if cn
111
+ "\"#{cn}\" <#{uri}>"
112
+ else
113
+ uri
114
+ end
115
+ end
116
+
117
+ def inspect
118
+ "#<Vpim::Icalendar::Address:cn=#{cn.inspect} status=#{partstat} rsvp?=#{rsvp} #{uri.inspect}>"
119
+ end
120
+
121
+ # The participation role for the calendar user specified by the address.
122
+ #
123
+ # The standard roles are:
124
+ # - CHAIR Indicates chair of the calendar entity
125
+ # - REQ-PARTICIPANT Indicates a participant whose participation is required
126
+ # - OPT-PARTICIPANT Indicates a participant whose participation is optional
127
+ # - NON-PARTICIPANT Indicates a participant who is copied for information purposes only
128
+ #
129
+ # The default role is REQ-PARTICIPANT, returned if no ROLE parameter was
130
+ # specified.
131
+ def role
132
+ return 'REQ-PARTICIPANT' unless r = @field.param('ROLE')
133
+ r.first.upcase
134
+ end
135
+
136
+ # The participation status for the calendar user specified by the
137
+ # property PARTSTAT, a String.
138
+ #
139
+ # These are the participation statuses for an Event:
140
+ # - NEEDS-ACTION Event needs action
141
+ # - ACCEPTED Event accepted
142
+ # - DECLINED Event declined
143
+ # - TENTATIVE Event tentatively accepted
144
+ # - DELEGATED Event delegated
145
+ #
146
+ # Default is NEEDS-ACTION.
147
+ #
148
+ # TODO - make the default depend on the component type.
149
+ def partstat
150
+ return 'NEEDS-ACTION' unless r = @field.param('PARTSTAT')
151
+ r.first.upcase
152
+ end
153
+
154
+ # Set or change the participation status of the address, the PARTSTAT,
155
+ # to +status+.
156
+ #
157
+ # See #partstat.
158
+ def partstat=(status)
159
+ @field['PARTSTAT'] = status.to_str
160
+ status
161
+ end
162
+
163
+ # The value of the RSVP field, either +true+ or +false+. It is used to
164
+ # specify whether there is an expectation of a favor of a reply from the
165
+ # calendar user specified by the property value.
166
+ #
167
+ # TODO - should be #rsvp?
168
+ def rsvp
169
+ return false unless r = @field.param('RSVP')
170
+ r = r.first
171
+ return false unless r
172
+ case r
173
+ when /TRUE/i then true
174
+ when /FALSE/i then false
175
+ else raise InvalidEncodingError, "RSVP param value not TRUE/FALSE: #{r}"
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+
@@ -0,0 +1,102 @@
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
+ require 'vpim/icalendar'
10
+
11
+ module Vpim
12
+
13
+ # Attachments are used by both iCalendar and vCard. They are either a URI or
14
+ # inline data, and their decoded value will be either a Uri or a Inline, as
15
+ # appropriate.
16
+ #
17
+ # Besides the methods specific to their class, both kinds of object implement
18
+ # a set of common methods, allowing them to be treated uniformly:
19
+ # - Uri#to_io, Inline#to_io: return an IO from which the value can be read.
20
+ # - Uri#to_s, Inline#to_s: return the value as a String.
21
+ # - Uri#format, Inline#format: the format of the value. This is supposed to
22
+ # be an "iana defined" identifier (like "image/jpeg"), but could be almost
23
+ # anything (or nothing) in practice. Since the parameter is optional, it may
24
+ # be "".
25
+ #
26
+ # The objects can also be distinguished by their class, if necessary.
27
+ module Attachment
28
+
29
+ # TODO - It might be possible to autodetect the format from the first few
30
+ # bytes of the value, and return the appropriate MIME type when format
31
+ # isn't defined.
32
+ #
33
+ # iCalendar and vCard put the format in different parameters, and the
34
+ # default kind of value is different.
35
+ def Attachment.decode(field, defkind, fmtparam) #:nodoc:
36
+ format = field.pvalue(fmtparam) || ''
37
+ kind = field.kind || defkind
38
+ case kind
39
+ when 'text'
40
+ Inline.new(Vpim.decode_text(field.value), format)
41
+ when 'uri'
42
+ Uri.new(field.value_raw, format)
43
+ when 'binary'
44
+ Inline.new(field.value, format)
45
+ else
46
+ raise InvalidEncodingError, "Attachment of type #{kind} is not allowed"
47
+ end
48
+ end
49
+
50
+ # Extends a String to support some of the same methods as Uri.
51
+ class Inline < String
52
+ def initialize(s, format) #:nodoc:
53
+ @format = format
54
+ super(s)
55
+ end
56
+
57
+ # Return an IO object for the inline data. See +stringio+ for more
58
+ # information.
59
+ def to_io
60
+ StringIO.new(self)
61
+ end
62
+
63
+ # The format of the inline data.
64
+ # See Attachment.
65
+ attr_reader :format
66
+ end
67
+
68
+ # Encapsulates a URI and implements some methods of String.
69
+ class Uri
70
+ def initialize(uri, format) #:nodoc:
71
+ @uri = uri
72
+ @format = format
73
+ end
74
+
75
+ # The URI value.
76
+ attr_reader :uri
77
+
78
+ # The format of the data referred to by the URI.
79
+ # See Attachment.
80
+ attr_reader :format
81
+
82
+ # Return an IO object from opening the URI. See +open-uri+ for more
83
+ # information.
84
+ def to_io
85
+ open(@uri)
86
+ end
87
+
88
+ # Return the String from reading the IO object to end-of-data.
89
+ def to_s
90
+ to_io.read(nil)
91
+ end
92
+
93
+ def inspect #:nodoc:
94
+ s = "<#{self.class.to_s}: #{uri.inspect}>"
95
+ s << ", #{@format.inspect}" if @format
96
+ s
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+
@@ -21,6 +21,16 @@ module Vpim
21
21
  # A vCard, for example, is a specialization of a directory info object.
22
22
  #
23
23
  # - [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
24
+ #
25
+ # Here's an example of encoding a simple vCard using the low-level APIs:
26
+ #
27
+ # card = Vpim::Vcard.create
28
+ # card << Vpim::DirectoryInfo::Field.create('EMAIL', 'user.name@example.com', 'TYPE' => 'INTERNET' )
29
+ # card << Vpim::DirectoryInfo::Field.create('URL', 'http://www.example.com/user' )
30
+ # card << Vpim::DirectoryInfo::Field.create('FN', 'User Name' )
31
+ # puts card.to_s
32
+ #
33
+ # Don't do it like that, use Vpim::Vcard::Maker.
24
34
  class DirectoryInfo
25
35
  include Enumerable
26
36
 
@@ -215,8 +225,8 @@ module Vpim
215
225
  # profile-specific fields can be deleted, including mandatory ones. For
216
226
  # vCards in particular, in order to avoid destroying them, I suggest
217
227
  # creating a new Vcard, and copying over all the fields that you still
218
- # want, rather than using #delete. This is easy with Maker::Vcard#copy, see
219
- # the Maker::Vcard examples.
228
+ # want, rather than using #delete. This is easy with Vcard::Maker#copy, see
229
+ # the Vcard::Maker examples.
220
230
  def delete(field)
221
231
  case
222
232
  when field.name?('BEGIN'), field.name?('END')
@@ -15,19 +15,18 @@ module Vpim
15
15
  class DirectoryInfo
16
16
 
17
17
  # A field in a directory info object.
18
- #
19
- # TODO
20
- # - Field should know which param values and field vales are
21
- # case-insensitive, configurably, so it can down case them
22
- # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or
23
- # pvalue_iset/idel/iadd, where set sets them all, add adds if not present,
24
- # and del deletes any that are present
25
- # - I really, really, need a case-insensitive string...
26
- # - should allow nil as a field value, its not the same as '', if there is
27
- # more than one pvalue, the empty string will show up. This isn't strictly
28
- # disallowed, but its odd. Should also strip empty strings on decoding, if
29
- # I don't already.
30
18
  class Field
19
+ # TODO
20
+ # - Field should know which param values and field values are
21
+ # case-insensitive, configurably, so it can down case them
22
+ # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or
23
+ # pvalue_iset/idel/iadd, where set sets them all, add adds if not present,
24
+ # and del deletes any that are present
25
+ # - I really, really, need a case-insensitive string...
26
+ # - should allow nil as a field value, its not the same as '', if there is
27
+ # more than one pvalue, the empty string will show up. This isn't strictly
28
+ # disallowed, but its odd. Should also strip empty strings on decoding, if
29
+ # I don't already.
31
30
  private_class_method :new
32
31
 
33
32
  def Field.create_array(fields)
@@ -263,6 +262,19 @@ module Vpim
263
262
  # FIXME - remove my own uses of #params
264
263
  alias params pnames # :nodoc:
265
264
 
265
+ # The first value of the param +name+, nil if there is no such param,
266
+ # the param has no value, or the first param value is zero-length.
267
+ def pvalue(name)
268
+ v = pvalues( name )
269
+ if v
270
+ v = v.first
271
+ end
272
+ if v
273
+ v = nil unless v.length > 0
274
+ end
275
+ v
276
+ end
277
+
266
278
  # The Array of all values of the param +name+, nil if there is no such
267
279
  # param, [] if the param has no values. If the Field isn't frozen, the
268
280
  # Array is mutable.
@@ -402,9 +414,9 @@ module Vpim
402
414
  if v.size > 1
403
415
  raise InvalidEncodingError, "multi-valued param 'VALUE' (#{values})"
404
416
  end
405
- v = v.first
417
+ v = v.first.downcase
406
418
  end
407
- v.downcase
419
+ v
408
420
  end
409
421
 
410
422
  # The value as an array of Time objects (all times and dates in
@@ -10,6 +10,8 @@ require 'vpim/rfc2425'
10
10
  require 'vpim/dirinfo'
11
11
  require 'vpim/rrule'
12
12
  require 'vpim/vevent'
13
+ require 'vpim/vtodo'
14
+ require 'vpim/vjournal'
13
15
  require 'vpim/vpim'
14
16
 
15
17
  module Vpim
@@ -64,26 +66,24 @@ module Vpim
64
66
  @properties = DirectoryInfo.create(outer)
65
67
  @properties.check_begin_end('VCALENDAR')
66
68
 
67
- # Categorize the components
68
- @vevents = []
69
- @vtodos = []
70
- @vjournals = []
71
- @others = []
69
+ @components = []
70
+
71
+ factory = {
72
+ 'VEVENT' => Vevent,
73
+ 'VTODO' => Vtodo,
74
+ 'VJOURNAL' => Vjournal,
75
+ }
72
76
 
73
77
  inner.each do |component|
74
- # First field in every component should be a "BEGIN:".
75
78
  name = component.first
76
- if ! name.name? 'BEGIN'
79
+ unless name.name? 'BEGIN'
77
80
  raise InvalidEncodingError, "calendar component begins with #{name.name}, instead of BEGIN!"
78
81
  end
79
82
 
80
- name = name.value.upcase
83
+ name = name.value
81
84
 
82
- case name
83
- when 'VEVENT' then @vevents << Vevent.new(component)
84
- when 'VTODO' then @vtodos << Vtodo.new(component)
85
- when 'VJOURNAL' then @vjournals << Vjournal.new(component)
86
- else @others << component
85
+ if klass = factory[name]
86
+ @components << klass.new(component)
87
87
  end
88
88
  end
89
89
  end
@@ -141,9 +141,7 @@ module Vpim
141
141
 
142
142
  last = fields.pop
143
143
 
144
- @vevents.each { |c| fields << c.fields }
145
- @vtodos.each { |c| fields << c.fields }
146
- @others.each { |c| fields << c.fields }
144
+ @components.each { |c| fields << c.fields }
147
145
 
148
146
  fields << last
149
147
  end
@@ -153,14 +151,10 @@ module Vpim
153
151
  # Push a calendar component onto the calendar.
154
152
  def push(component)
155
153
  case component
156
- when Vevent
157
- @vevents << component
158
- when Vtodo
159
- @vtodos << component
160
- when Vjournal
161
- @vjournals << component
154
+ when Vevent, Vtodo, Vjournal
155
+ @components << component
162
156
  else
163
- raise ArgumentError, "can't add component type #{component.type} to a calendar"
157
+ raise ArgumentError, "can't add a #{component.type} to a calendar"
164
158
  end
165
159
  end
166
160
 
@@ -270,196 +264,56 @@ module Vpim
270
264
  m ? m.upcase : m
271
265
  end
272
266
 
273
- # The array of all calendar event components (each is a Vevent).
267
+ # The value of the CALSCALE: property, or "GREGORIAN" if CALSCALE: is not
268
+ # present.
274
269
  #
275
- # TODO - should this take an interval: t0,t1?
276
- def events
277
- @vevents
278
- end
279
-
280
- # The array of all calendar todo components (each is a Vtodo).
281
- def todos
282
- @vtodos
270
+ # This is of academic interest, really because there aren't any other
271
+ # calendar scales defined, and given that its hard enough just dealing with
272
+ # Gregorian calendars, there probably won't be.
273
+ def calscale
274
+ proptext('CALSCALE') || 'GREGORIAN'
283
275
  end
284
276
 
285
- # The array of all calendar journal components (each is a Vjournal).
286
- def journals
287
- @vjournals
288
- end
289
- end
290
-
291
- end
292
-
293
- =begin
294
-
295
- Notes on a CAL-ADDRESS
296
-
297
- When used with ATTENDEE, the parameters are:
298
- CN
299
- CUTYPE
300
- DELEGATED-FROM
301
- DELEGATED-TO
302
- DIR
303
- LANGUAGE
304
- MEMBER
305
- PARTSTAT
306
- ROLE
307
- RSVP
308
- SENT-BY
309
-
310
- When used with ORGANIZER, the parameters are:
311
- CN
312
- DIR
313
- LANGUAGE
314
- SENT-BY
315
-
316
-
317
- What I've seen in Notes invitations, and iCal responses:
318
- ROLE
319
- PARTSTAT
320
- RSVP
321
- CN
322
-
323
- Support these last 4, for now.
324
-
325
- =end
326
-
327
- module Vpim
328
- class Icalendar
329
- # Used to represent calendar fields containing CAL-ADDRESS values.
330
- # The organizer or the attendees of a calendar event are examples of such
331
- # a field.
277
+ # The array of all supported calendar components. If a class is provided,
278
+ # return only the components of that class.
332
279
  #
333
- # Example:
334
- # ORGANIZER;CN="A. Person":mailto:a_person@example.com
335
- # ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION
336
- # ;CN="Sam Roberts";RSVP=TRUE:mailto:SRoberts@example.com
280
+ # If a block is provided, yield the components instead of returning them.
337
281
  #
338
- class Address
339
-
340
- # Create an Address from a DirectoryInfo::Field, +field+.
341
- #
342
- # TODO - make private, and split into the encode/decode/create trinity.
343
- def initialize(field)
344
- unless field.value
345
- raise ArgumentError
346
- end
347
-
348
- @field = field
349
- end
350
-
351
- # Return a representation of this Address as a DirectoryInfo::Field.
352
- def field
353
- @field.copy
354
- end
355
-
356
- # Create a copy of Address. If the original Address was frozen, this one
357
- # won't be.
358
- def copy
359
- Marshal.load(Marshal.dump(self))
360
- end
361
-
362
- # Addresses in a CAL-ADDRESS are represented as a URI, usually a mailto URI.
363
- def uri
364
- @field.value
365
- end
366
-
367
- # Return true if the +uri+ is == to this address' URI. The comparison
368
- # is case-insensitive.
369
- #
370
- # FIXME - why case insensitive? Email addresses. Should use a URI library
371
- # if I can find one and it knows how to do URI comparisons.
372
- def ==(uri)
373
- Vpim::Methods.casecmp?(self.uri.to_str, uri.to_str)
374
- end
375
-
376
- # The common or displayable name associated with the calendar address,
377
- # or nil if there is none.
378
- def cn
379
- return nil unless n = @field.param('CN')
282
+ # Examples:
283
+ # calendar.components(Vpim::Icalendar::Vevent)
284
+ # => array of all calendar components
285
+ #
286
+ # calendar.components(Vpim::Icalendar::Vtodo) {|c| c... }
287
+ # => yield all todo components
288
+ #
289
+ # calendar.components {|c| c... }
290
+ # => yield all components
291
+ def components(klass=Object) #:yields:component
292
+ # TODO - should this take an interval: t0,t1?
380
293
 
381
- # FIXME = the CN param may have no value, which is an error, but don't try
382
- # to decode it, return either nil, or InvalidEncoding
383
- Vpim.decode_text(n.first)
294
+ unless block_given?
295
+ return @components.select{|c| klass === c}.freeze
384
296
  end
385
297
 
386
- # A string representation of an address, using the common name, and the
387
- # URI. The URI protocol is stripped if it's "mailto:".
388
- #
389
- # TODO - this needs to properly escape the cn string!
390
- def to_s
391
- u = uri
392
- u.gsub!(/^mailto: */i, '')
393
-
394
- if cn
395
- "\"#{cn}\" <#{uri}>"
396
- else
397
- uri
298
+ @components.each do |c|
299
+ if klass === c
300
+ yield c
398
301
  end
399
302
  end
303
+ self
304
+ end
400
305
 
401
- def inspect
402
- "#<Vpim::Icalendar::Address:cn=#{cn.inspect} status=#{partstat} rsvp?=#{rsvp} #{uri.inspect}>"
403
- end
404
-
405
- # The participation role for the calendar user specified by the address.
406
- #
407
- # The standard roles are:
408
- # - CHAIR Indicates chair of the calendar entity
409
- # - REQ-PARTICIPANT Indicates a participant whose participation is required
410
- # - OPT-PARTICIPANT Indicates a participant whose participation is optional
411
- # - NON-PARTICIPANT Indicates a participant who is copied for information purposes only
412
- #
413
- # The default role is REQ-PARTICIPANT, returned if no ROLE parameter was
414
- # specified.
415
- def role
416
- return 'REQ-PARTICIPANT' unless r = @field.param('ROLE')
417
- r.first.upcase
418
- end
419
-
420
- # The participation status for the calendar user specified by the
421
- # property PARTSTAT, a String.
422
- #
423
- # These are the participation statuses for an Event:
424
- # - NEEDS-ACTION Event needs action
425
- # - ACCEPTED Event accepted
426
- # - DECLINED Event declined
427
- # - TENTATIVE Event tentatively accepted
428
- # - DELEGATED Event delegated
429
- #
430
- # Default is NEEDS-ACTION.
431
- #
432
- # TODO - make the default depend on the component type.
433
- def partstat
434
- return 'NEEDS-ACTION' unless r = @field.param('PARTSTAT')
435
- r.first.upcase
436
- end
437
-
438
- # Set or change the participation status of the address, the PARTSTAT,
439
- # to +status+.
440
- #
441
- # See #partstat.
442
- def partstat=(status)
443
- @field['PARTSTAT'] = status.to_str
444
- status
445
- end
306
+ # For backwards compatibility. Use #components.
307
+ def events #:nodoc:
308
+ components Icalendar::Vevent
309
+ end
446
310
 
447
- # The value of the RSVP field, either +true+ or +false+. It is used to
448
- # specify whether there is an expectation of a favor of a reply from the
449
- # calendar user specified by the property value.
450
- #
451
- # TODO - should be #rsvp?
452
- def rsvp
453
- return false unless r = @field.param('RSVP')
454
- r = r.first
455
- return false unless r
456
- case r
457
- when /TRUE/i then true
458
- when /FALSE/i then false
459
- else raise InvalidEncodingError, "RSVP param value not TRUE/FALSE: #{r}"
460
- end
461
- end
311
+ # For backwards compatibility. Use #components.
312
+ def todos #:nodoc:
313
+ components Icalendar::Vtodo
462
314
  end
315
+
463
316
  end
317
+
464
318
  end
465
319