vpim 0.323 → 0.357

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