vpim 0.16 → 0.17

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