vpim 0.16 → 0.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: icalendar.rb,v 1.24 2005/01/07 03:32:44 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
@@ -264,7 +262,7 @@ module Vpim
264
262
 
265
263
  # Check if the protocol method is +method+
266
264
  def protocol?(method)
267
- protocol == method.upcase
265
+ Vpim::Methods.casecmp?(protocol, method)
268
266
  end
269
267
 
270
268
  def Icalendar.decode_duration(str) #:nodoc:
@@ -313,17 +311,10 @@ module Vpim
313
311
  # END:VCALENDAR), multiple iCalendars can be concatenated into a single
314
312
  # file.
315
313
  #
316
- # cal must be either a string, or an IO object.
317
- def Icalendar.decode(cal)
318
- if cal.respond_to? :to_str
319
- string = cal.to_str
320
- elsif cal.kind_of? IO
321
- string = cal.read(nil)
322
- else
323
- raise ArgumentError, "Icalendar.decode cannot be called with a #{cal.type}"
324
- end
325
-
326
- entities = Vpim.expand(Vpim.decode(string))
314
+ # cal must be String or IO, or implement #each by returning
315
+ # each line in the input as those classes do.
316
+ def Icalendar.decode(cal, e = nil)
317
+ entities = Vpim.expand(Vpim.decode(cal))
327
318
 
328
319
  # Since all iCalendars must have a begin/end, the top-level should
329
320
  # consist entirely of entities/arrays, even if its a single iCalendar.
@@ -465,8 +456,11 @@ module Vpim
465
456
 
466
457
  # Return true if the +uri+ is == to this address' URI. The comparison
467
458
  # is case-insensitive.
459
+ #
460
+ # FIXME - why case insensitive? Email addresses. Should use a URI library
461
+ # if I can find one and it knows how to do URI comparisons.
468
462
  def ==(uri)
469
- self.uri.downcase == uri.downcase
463
+ Vpim::Methods.casecmp?(self.uri.to_str, uri.to_str)
470
464
  end
471
465
 
472
466
  # The common or displayable name associated with the calendar address,
@@ -0,0 +1,548 @@
1
+ =begin
2
+ $Id: icalendar.rb,v 1.24 2005/01/07 03:32:44 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/dirinfo'
13
+ require 'vpim/rrule'
14
+ require 'vpim/vevent'
15
+ require 'vpim/vpim'
16
+
17
+ =begin
18
+
19
+ ... ; y/n (whether I've seen it in Apple's calendars)
20
+ name
21
+ section
22
+ type/comments
23
+
24
+
25
+ icalbody = icalprops component
26
+
27
+ icalprops =
28
+ prodid / ; y PRODID 4.7.3 required, TEXT
29
+ version / ; y 4.7.4 required, TEXT, "2.0"
30
+ calscal / ; y 4.7.1 only defined value is GREGORIAN
31
+ method / ; n METHOD 4.7.2 used with transport protocols
32
+
33
+ component =
34
+ eventc / ; y VEVENT 4.6.1
35
+ todoc / ; y VTODO 4.6.2
36
+ journalc / ; n
37
+ freebusyc / ; n
38
+ timezonec / ; n
39
+
40
+ alarmc ; y VALARM 4.6.6 occurs inside a VEVENT or a VTODO
41
+
42
+ class ; y CLASS 4.8.1.3 private/public/confidentical/... (default=public)
43
+
44
+ comment ; n 4.8.1.4 TEXT
45
+ description ; y 4.8.1.5 TEXT
46
+ summary ; y 4.8.1.12 TEXT
47
+ location ; y 4.8.1.7 TEXT intended venue
48
+
49
+ priority ; n why? 4.8.1.9 INTEGER, why isn't this seen for my TODO items?
50
+
51
+ status ; y 4.8.1.11 TEXT, different values defined for event, todo, journal
52
+
53
+ Event: TENTATIVE, CONFIRMED, CANCELLED
54
+
55
+ Todo: NEEDS-ACTION, COMPLETED, IN-PROCESS, CANCELLED
56
+
57
+ Journal: DRAFT, FINAL, CANCELLED
58
+
59
+ dtstart ; y DTSTART 4.8.2.4 DATE-TIME is default, value=date can be set
60
+ dtend ; y DTEND 4.8.2.2 Unless it has Z (UTC), or a tzid, then it is local-time.
61
+
62
+ dtstamp ; y DTSTAMP 4.8.7.2 DATE-TIME, creation time, inclusion is mandatory, but what does
63
+ it mean? It seems to be when the icalendar was actually created (as opposed to when the user entered
64
+ the information into the calendar database, for example), but in that case my Apple icalendars should
65
+ have all components having the same DTSTAMP, but they don't!
66
+
67
+ duration ; y DURATION 4.8.2.5 dur-value
68
+
69
+ dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
70
+
71
+ dur-date = dur-day [dur-time]
72
+ = 1*DIGIT "D" [ dur-time ]
73
+
74
+ dur-time = "T" (dur-hour / dur-minute / dur-second)
75
+ = "T" (
76
+ 1*DIGIT "H" [ 1*DIGIT "M" [ 1*DIGIT "S" ] ] /
77
+ 1*DIGIT "M" [ 1*DIGIT "S" ] /
78
+ 1*DIGIT "S"
79
+ )
80
+
81
+ dur-week = 1*DIGIT "W"
82
+ dur-day = 1*DIGIT "D"
83
+ dur-hour = 1*DIGIT "H" [dur-minute]
84
+ dur-minute = 1*DIGIT "M" [dur-second]
85
+ dur-second = 1*DIGIT "S"
86
+
87
+ The EBNF is complicated, because they want to say that /some/ component
88
+ must be present, and that if you have a "T", you need a time after it,
89
+ and that you can't have an hour followed by seconds with no intervening
90
+ minutes... but we don't care about that during decoding, so we rewrite
91
+ the EBNF as:
92
+
93
+ dur-value = ["+" / "-"] "P" [ 1*DIGIT "W" ] [ 1*DIGIT "D" ] [ "T" [ 1*DIGIT "H" ] [ 1*DIGIT "M" ] [ 1*DIGIT "S" ] ]
94
+
95
+ dtdue ; n DTDUE 4.8.2.3
96
+
97
+ uid ; y UID 4.8.4.7 TEXT, recommended to generate them in RFC822 form
98
+
99
+ rrule ; y RRULE 4.8.5.4 RECUR, can occur multiple times!
100
+
101
+
102
+
103
+ VEVENT Ifx:
104
+
105
+ TEXT: summary, description, comment, location, uid
106
+
107
+ think about: uid
108
+
109
+ Vevent#status -> The status, upper-case.
110
+ Vevent#status= -> set a new status, only allow the defined statuses!
111
+ Vevent#status?(s) -> check if the status is s
112
+
113
+ can contain alarms... should it include an alarms module?
114
+
115
+ =end
116
+
117
+ module Vpim
118
+ # An iCalendar.
119
+ #
120
+ # A Calendar is some meta-information followed by a sequence of components.
121
+ #
122
+ # Defined components are Event, Todo, Freebusy, Journal, and Timezone, each
123
+ # of which are represented by their own class, though they share many
124
+ # properties in common. For example, Event and Todo may both contain
125
+ # multiple Alarm components.
126
+ #
127
+ # = Reference
128
+ #
129
+ # The iCalendar format is specified by a series of IETF documents:
130
+ #
131
+ # - link:rfc2445.txt: Internet Calendaring and Scheduling Core Object Specification
132
+ # - link:rfc2446.txt: iCalendar Transport-Independent Interoperability Protocol
133
+ # (iTIP) Scheduling Events, BusyTime, To-dos and Journal Entries
134
+ # - link:rfc2447.txt: iCalendar Message-Based Interoperability Protocol
135
+ #
136
+ # iCalendar (RFC 2445) is based on vCalendar, but does not appear to be
137
+ # altogether compatible. iCalendar files have VERSION:2.0 and vCalendar have
138
+ # VERSION:1.0. While much appears to be similar, the recurrence rule syntax,
139
+ # at least, is completely different.
140
+ #
141
+ # iCalendars are usually transmitted in files with <code>.ics</code>
142
+ # extensions.
143
+ class Icalendar
144
+ include Vpim
145
+
146
+ # Regular expression strings for the EBNF of RFC 2445
147
+ module Bnf #:nodoc:
148
+ # dur-value = ["+" / "-"] "P" [ 1*DIGIT "W" ] [ 1*DIGIT "D" ] [ "T" [ 1*DIGIT "H" ] [ 1*DIGIT "M" ] [ 1*DIGIT "S" ] ]
149
+ DURATION = '([-+])?P(\d+W)?(\d+D)?T?(\d+H)?(\d+M)?(\d+S)?'
150
+ end
151
+
152
+ private_class_method :new
153
+
154
+ # Create a new Icalendar object from +fields+, an array of
155
+ # DirectoryInfo::Field objects.
156
+ #
157
+ # When decoding Calendar data, you would usually use Icalendar.decode(),
158
+ # which decodes the data into the field arrays, and calls this method
159
+ # for each Calendar it finds.
160
+ def initialize(fields) #:nodoc:
161
+ # seperate into the outer-level fields, and the arrays of component
162
+ # fields
163
+ outer, inner = Vpim.outer_inner(fields)
164
+
165
+ # Make a dirinfo out of outer, and check its an iCalendar
166
+ @properties = DirectoryInfo.create(outer)
167
+ @properties.check_begin_end('VCALENDAR')
168
+
169
+ # Categorize the components
170
+ @vevents = []
171
+ @vtodos = []
172
+ @others = []
173
+
174
+ inner.each do |component|
175
+ # First field in every component should be a "BEGIN:".
176
+ name = component.first
177
+ if ! name.name? 'begin'
178
+ raise InvalidEncodingError, "calendar component begins with #{name.name}, instead of BEGIN!"
179
+ end
180
+
181
+ name = name.value.upcase
182
+
183
+ case name
184
+ when 'VEVENT' then @vevents << Vevent.new(component)
185
+ when 'VTODO' then @vtodos << Vtodo.new(component)
186
+ else @others << component
187
+ end
188
+ end
189
+ end
190
+
191
+ # Create a new Icalendar object with the minimal set of fields for a valid
192
+ # Calendar. If specified, +fields+ must be an array of
193
+ # DirectoryInfo::Field objects to add. They can override the the default
194
+ # Calendar fields, so, for example, this can be used to set a custom PRODID field.
195
+ #
196
+ # TODO - allow hash args like Vevent.create
197
+ def Icalendar.create(fields=[])
198
+ di = DirectoryInfo.create( [ DirectoryInfo::Field.create('VERSION', '2.0') ], 'VCALENDAR' )
199
+
200
+ DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
201
+
202
+ di.push_unique DirectoryInfo::Field.create('PRODID', "-//Ensemble Independant//vPim #{Vpim.version}//EN")
203
+ di.push_unique DirectoryInfo::Field.create('CALSCALE', "Gregorian")
204
+
205
+ new(di.to_a)
206
+ end
207
+
208
+ # Create a new Icalendar object with a protocol method of REPLY.
209
+ #
210
+ # Meeting requests, and such, are Calendar containers with a protocol
211
+ # method of REQUEST, and contains some number of Events, Todos, etc.,
212
+ # that may need replying to. In order to reply to any of these components
213
+ # of a request, you must first build a Calendar object to hold your reply
214
+ # components.
215
+ #
216
+ # This method builds the reply Calendar, you then will add to it replies
217
+ # to the specific components of the request Calendar that you are replying
218
+ # to. If you have any particular fields that you want to be in the
219
+ # Calendar, other than the defaults, then can be supplied as +fields+, an
220
+ # array of Field objects.
221
+ def Icalendar.create_reply(fields=[])
222
+ fields << DirectoryInfo::Field.create('METHOD', 'REPLY')
223
+
224
+ Icalendar.create(fields)
225
+ end
226
+
227
+ # Encode the Calendar as a string. The width is the maximum width of the
228
+ # encoded lines, it can be specified, but is better left to the default.
229
+ #
230
+ # TODO - only does top-level now, needs to add the events/todos/etc.
231
+ def encode(width=nil)
232
+ # We concatenate the fields of all objects, create a DirInfo, then
233
+ # encode it.
234
+ di = DirectoryInfo.create(self.fields.flatten)
235
+ di.encode(width)
236
+ end
237
+
238
+ # Used during encoding.
239
+ def fields # :nodoc:
240
+ fields = @properties.to_a
241
+
242
+ last = fields.pop
243
+
244
+ @vevents.each { |c| fields << c.fields }
245
+ @vtodos.each { |c| fields << c.fields }
246
+ @others.each { |c| fields << c.fields }
247
+
248
+ fields << last
249
+ end
250
+
251
+ alias to_s encode
252
+
253
+ # Push a calendar component onto the calendar.
254
+ def push(component)
255
+ case component
256
+ when Vevent
257
+ @vevents << component
258
+ when Vtodo
259
+ @vtodos << component
260
+ else
261
+ raise ArgumentError, "can't add component type #{component.type} to a calendar"
262
+ end
263
+ end
264
+
265
+ # Check if the protocol method is +method+
266
+ def protocol?(method)
267
+ protocol == method.upcase
268
+ end
269
+
270
+ def Icalendar.decode_duration(str) #:nodoc:
271
+ unless match = %r{\s*#{Bnf::DURATION}\s*}.match(str)
272
+ raise InvalidEncodingError, "duration not valid (#{str})"
273
+ end
274
+ dur = 0
275
+
276
+ # Remember: match[0] is the whole match string, match[1] is $1, etc.
277
+
278
+ # Week
279
+ if match[2]
280
+ dur = match[2].to_i
281
+ end
282
+ # Days
283
+ dur *= 7
284
+ if match[3]
285
+ dur += match[3].to_i
286
+ end
287
+ # Hours
288
+ dur *= 24
289
+ if match[4]
290
+ dur += match[4].to_i
291
+ end
292
+ # Minutes
293
+ dur *= 60
294
+ if match[5]
295
+ dur += match[5].to_i
296
+ end
297
+ # Seconds
298
+ dur *= 60
299
+ if match[6]
300
+ dur += match[6].to_i
301
+ end
302
+
303
+ if match[1] && match[1] == '-'
304
+ dur = -dur
305
+ end
306
+
307
+ dur
308
+ end
309
+
310
+ # Decode iCalendar data into an array of Icalendar objects.
311
+ #
312
+ # Since iCalendars are self-delimited (by a BEGIN:VCALENDAR and an
313
+ # END:VCALENDAR), multiple iCalendars can be concatenated into a single
314
+ # file.
315
+ #
316
+ # cal must be String or IO, or implement #each by returning
317
+ # each line in the input as those classes do.
318
+ def Icalendar.decode(cal, e = nil)
319
+ entities = Vpim.expand(Vpim.decode(cal))
320
+
321
+ # Since all iCalendars must have a begin/end, the top-level should
322
+ # consist entirely of entities/arrays, even if its a single iCalendar.
323
+ if entities.detect { |e| ! e.kind_of? Array }
324
+ raise "Not a valid iCalendar"
325
+ end
326
+
327
+ calendars = []
328
+
329
+ entities.each do |e|
330
+ calendars << new(e)
331
+ end
332
+
333
+ calendars
334
+ end
335
+
336
+ # The iCalendar version multiplied by 10 as an Integer. If no VERSION field
337
+ # is present (which is non-conformant), nil is returned. iCalendar must
338
+ # have a version of 20, and vCalendar would have a version of 10.
339
+ def version
340
+ v = @properties['VERSION']
341
+
342
+ unless v
343
+ raise InvalidEncodingError, "Invalid calendar, no version field!"
344
+ end
345
+
346
+ v = v.to_f * 10
347
+ v = v.to_i
348
+ end
349
+
350
+ # The value of the PRODID field, an unstructured string meant to
351
+ # identify the software which encoded the Calendar data.
352
+ def producer
353
+ #f = @properties.field('PRODID')
354
+ #f && f.to_text
355
+ @properties.text('PRODID').first
356
+ end
357
+
358
+ # The value of the METHOD field. Protocol methods are used when iCalendars
359
+ # are exchanged in a calendar messaging system, such as iTIP or iMIP. When
360
+ # METHOD is not specified, the Calendar object is merely being used to
361
+ # transport a snapshot of some calendar information; without the intention
362
+ # of conveying a scheduling semantic.
363
+ #
364
+ # Note that this can't be called 'method', that name is reserved.
365
+ def protocol
366
+ m = @properties['METHOD']
367
+ m ? m.upcase : m
368
+ end
369
+
370
+ # The array of all calendar events (each is a Vevent).
371
+ #
372
+ # TODO - should this take an interval: t0,t1?
373
+ def events
374
+ @vevents
375
+ end
376
+
377
+ # The array of all calendar todos (each is a Vtodo).
378
+ def todos
379
+ @vtodos
380
+ end
381
+ end
382
+
383
+ end
384
+
385
+ =begin
386
+
387
+ Notes on a CAL-ADDRESS
388
+
389
+ When used with ATTENDEE, the parameters are:
390
+ CN
391
+ CUTYPE
392
+ DELEGATED-FROM
393
+ DELEGATED-TO
394
+ DIR
395
+ LANGUAGE
396
+ MEMBER
397
+ PARTSTAT
398
+ ROLE
399
+ RSVP
400
+ SENT-BY
401
+
402
+ When used with ORGANIZER, the parameters are:
403
+ CN
404
+ DIR
405
+ LANGUAGE
406
+ SENT-BY
407
+
408
+
409
+ What I've seen in Notes invitations, and iCal responses:
410
+ ROLE
411
+ PARTSTAT
412
+ RSVP
413
+ CN
414
+
415
+ Support these last 4, for now.
416
+
417
+ =end
418
+
419
+ module Vpim
420
+ class Icalendar
421
+ # Used to represent calendar fields containing CAL-ADDRESS values.
422
+ # The organizer or the attendees of a calendar event are examples of such
423
+ # a field.
424
+ #
425
+ # Example:
426
+ # ORGANIZER;CN="A. Person":mailto:a_person@example.com
427
+ # ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION
428
+ # ;CN="Sam Roberts";RSVP=TRUE:mailto:SRoberts@example.com
429
+ #
430
+ class Address
431
+
432
+ # Create an Address from a DirectoryInfo::Field, +field+.
433
+ #
434
+ # TODO - make private, and split into the encode/decode/create trinity.
435
+ def initialize(field)
436
+ unless field.value
437
+ raise ArgumentError
438
+ end
439
+
440
+ @field = field
441
+ end
442
+
443
+ # Return a representation of this Address as a DirectoryInfo::Field.
444
+ def field
445
+ @field.copy
446
+ end
447
+
448
+ # Create a copy of Address. If the original Address was frozen, this one
449
+ # won't be.
450
+ def copy
451
+ Marshal.load(Marshal.dump(self))
452
+ end
453
+
454
+ # Addresses in a CAL-ADDRESS are represented as a URI, usually a mailto URI.
455
+ def uri
456
+ @field.value
457
+ end
458
+
459
+ # Return true if the +uri+ is == to this address' URI. The comparison
460
+ # is case-insensitive.
461
+ def ==(uri)
462
+ self.uri.downcase == uri.downcase
463
+ end
464
+
465
+ # The common or displayable name associated with the calendar address,
466
+ # or nil if there is none.
467
+ def cn
468
+ return nil unless n = @field.param('CN')
469
+
470
+ # FIXME = the CN param may have no value, which is an error, but don't try
471
+ # to decode it, return either nil, or InvalidEncoding
472
+ Vpim.decode_text(n.first)
473
+ end
474
+
475
+ # A string representation of an address, using the common name, and the
476
+ # URI. The URI protocol is stripped if it's "mailto:".
477
+ #
478
+ # TODO - this needs to properly escape the cn string!
479
+ def to_s
480
+ u = uri
481
+ u.gsub!(/^mailto: */i, '')
482
+
483
+ if cn
484
+ "\"#{cn}\" <#{uri}>"
485
+ else
486
+ uri
487
+ end
488
+ end
489
+
490
+ # The participation role for the calendar user specified by the address.
491
+ #
492
+ # The standard roles are:
493
+ # - CHAIR Indicates chair of the calendar entity
494
+ # - REQ-PARTICIPANT Indicates a participant whose participation is required
495
+ # - OPT-PARTICIPANT Indicates a participant whose participation is optional
496
+ # - NON-PARTICIPANT Indicates a participant who is copied for information purposes only
497
+ #
498
+ # The default role is REQ-PARTICIPANT, returned if no ROLE parameter was
499
+ # specified.
500
+ def role
501
+ return 'REQ-PARTICIPANT' unless r = @field.param('ROLE')
502
+ r.first.upcase
503
+ end
504
+
505
+ # The participation status for the calendar user specified by the
506
+ # property PARTSTAT, a String.
507
+ #
508
+ # These are the participation statuses for an Event:
509
+ # - NEEDS-ACTION Event needs action
510
+ # - ACCEPTED Event accepted
511
+ # - DECLINED Event declined
512
+ # - TENTATIVE Event tentatively accepted
513
+ # - DELEGATED Event delegated
514
+ #
515
+ # Default is NEEDS-ACTION.
516
+ #
517
+ # TODO - make the default depend on the component type.
518
+ def partstat
519
+ return 'NEEDS-ACTION' unless r = @field.param('PARTSTAT')
520
+ r.first.upcase
521
+ end
522
+
523
+ # Set or change the participation status of the address, the PARTSTAT,
524
+ # to +status+.
525
+ #
526
+ # See #partstat.
527
+ def partstat=(status)
528
+ @field['partstat'] = status.to_str
529
+ status
530
+ end
531
+
532
+ # The value of the RSVP field, either +true+ or +false+. It is used to
533
+ # specify whether there is an expectation of a favor of a reply from the
534
+ # calendar user specified by the property value.
535
+ def rsvp
536
+ return false unless r = @field.param('RSVP')
537
+ r = r.first
538
+ return false unless r
539
+ case r
540
+ when /TRUE/i then true
541
+ when /FALSE/i then false
542
+ else raise InvalidEncodingError, "RSVP param value not TRUE/FALSE: #{r}"
543
+ end
544
+ end
545
+ end
546
+ end
547
+ end
548
+