vpim-rails 0.661

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/CHANGES +504 -0
  2. data/COPYING +58 -0
  3. data/README +182 -0
  4. data/lib/atom.rb +728 -0
  5. data/lib/plist.rb +22 -0
  6. data/lib/vpim.rb +13 -0
  7. data/lib/vpim/address.rb +219 -0
  8. data/lib/vpim/attachment.rb +102 -0
  9. data/lib/vpim/date.rb +222 -0
  10. data/lib/vpim/dirinfo.rb +277 -0
  11. data/lib/vpim/duration.rb +119 -0
  12. data/lib/vpim/enumerator.rb +32 -0
  13. data/lib/vpim/field.rb +614 -0
  14. data/lib/vpim/icalendar.rb +382 -0
  15. data/lib/vpim/maker/vcard.rb +16 -0
  16. data/lib/vpim/property/base.rb +193 -0
  17. data/lib/vpim/property/common.rb +315 -0
  18. data/lib/vpim/property/location.rb +38 -0
  19. data/lib/vpim/property/priority.rb +43 -0
  20. data/lib/vpim/property/recurrence.rb +69 -0
  21. data/lib/vpim/property/resources.rb +24 -0
  22. data/lib/vpim/repo.rb +181 -0
  23. data/lib/vpim/rfc2425.rb +367 -0
  24. data/lib/vpim/rrule.rb +599 -0
  25. data/lib/vpim/time.rb +40 -0
  26. data/lib/vpim/vcard.rb +1429 -0
  27. data/lib/vpim/version.rb +18 -0
  28. data/lib/vpim/vevent.rb +187 -0
  29. data/lib/vpim/view.rb +90 -0
  30. data/lib/vpim/vjournal.rb +58 -0
  31. data/lib/vpim/vpim.rb +65 -0
  32. data/lib/vpim/vtodo.rb +103 -0
  33. data/samples/README.mutt +93 -0
  34. data/samples/ab-query.rb +57 -0
  35. data/samples/cmd-itip.rb +156 -0
  36. data/samples/ex_cpvcard.rb +55 -0
  37. data/samples/ex_get_vcard_photo.rb +22 -0
  38. data/samples/ex_mkv21vcard.rb +34 -0
  39. data/samples/ex_mkvcard.rb +64 -0
  40. data/samples/ex_mkyourown.rb +29 -0
  41. data/samples/ics-dump.rb +210 -0
  42. data/samples/ics-to-rss.rb +84 -0
  43. data/samples/mutt-aliases-to-vcf.rb +45 -0
  44. data/samples/osx-wrappers.rb +86 -0
  45. data/samples/reminder.rb +203 -0
  46. data/samples/rrule.rb +71 -0
  47. data/samples/tabbed-file-to-vcf.rb +390 -0
  48. data/samples/vcf-dump.rb +86 -0
  49. data/samples/vcf-lines.rb +61 -0
  50. data/samples/vcf-to-ics.rb +22 -0
  51. data/samples/vcf-to-mutt.rb +121 -0
  52. data/test/test_all.rb +17 -0
  53. data/test/test_date.rb +120 -0
  54. data/test/test_dur.rb +41 -0
  55. data/test/test_field.rb +156 -0
  56. data/test/test_ical.rb +415 -0
  57. data/test/test_repo.rb +158 -0
  58. data/test/test_rrule.rb +1030 -0
  59. data/test/test_vcard.rb +973 -0
  60. data/test/test_view.rb +79 -0
  61. metadata +126 -0
@@ -0,0 +1,382 @@
1
+ =begin
2
+ Copyright (C) 2008 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 "enumerator"
10
+
11
+ require 'vpim/rfc2425'
12
+ require 'vpim/dirinfo'
13
+ require 'vpim/rrule'
14
+ require 'vpim/vevent'
15
+ require 'vpim/vtodo'
16
+ require 'vpim/vjournal'
17
+ require 'vpim/vpim'
18
+
19
+ module Vpim
20
+ # An iCalendar.
21
+ #
22
+ # A Calendar is some meta-information followed by a sequence of components.
23
+ #
24
+ # Defined components are Event, Todo, Freebusy, Journal, and Timezone, each
25
+ # of which are represented by their own class, though they share many
26
+ # properties in common. For example, Event and Todo may both contain
27
+ # multiple Alarm components.
28
+ #
29
+ # = Reference
30
+ #
31
+ # The iCalendar format is specified by a series of IETF documents:
32
+ #
33
+ # - link:rfc2445.txt: Internet Calendaring and Scheduling Core Object Specification
34
+ # - link:rfc2446.txt: iCalendar Transport-Independent Interoperability Protocol
35
+ # (iTIP) Scheduling Events, BusyTime, To-dos and Journal Entries
36
+ # - link:rfc2447.txt: iCalendar Message-Based Interoperability Protocol
37
+ #
38
+ # = iCalendar and vCalendar
39
+ #
40
+ # iCalendar files have VERSION:2.0 and vCalendar have VERSION:1.0. iCalendar
41
+ # (RFC 2445) is based on vCalendar, but but is not very compatible. While
42
+ # much appears to be similar, the recurrence rule syntax is completely
43
+ # different.
44
+ #
45
+ # iCalendars are usually transmitted in files with <code>.ics</code>
46
+ # extensions.
47
+ class Icalendar
48
+ # FIXME do NOT do this!
49
+ include Vpim
50
+
51
+ # Regular expression strings for the EBNF of RFC 2445
52
+ module Bnf #:nodoc:
53
+ # dur-value = ["+" / "-"] "P" [ 1*DIGIT "W" ] [ 1*DIGIT "D" ] [ "T" [ 1*DIGIT "H" ] [ 1*DIGIT "M" ] [ 1*DIGIT "S" ] ]
54
+ DURATION = '([-+])?P(\d+W)?(\d+D)?T?(\d+H)?(\d+M)?(\d+S)?'
55
+ end
56
+
57
+ private_class_method :new
58
+
59
+ # Create a new Icalendar object from +fields+, an array of
60
+ # DirectoryInfo::Field objects.
61
+ #
62
+ # When decoding Calendar data, you would usually use Icalendar.decode(),
63
+ # which decodes the data into the field arrays, and calls this method
64
+ # for each Calendar it finds.
65
+ def initialize(fields) #:nodoc:
66
+ # seperate into the outer-level fields, and the arrays of component
67
+ # fields
68
+ outer, inner = Vpim.outer_inner(fields)
69
+
70
+ # Make a dirinfo out of outer, and check its an iCalendar
71
+ @properties = DirectoryInfo.create(outer)
72
+ @properties.check_begin_end('VCALENDAR')
73
+
74
+ @components = []
75
+
76
+ # could use #constants instead of this
77
+ factory = {
78
+ 'VEVENT' => Vevent,
79
+ 'VTODO' => Vtodo,
80
+ 'VJOURNAL' => Vjournal,
81
+ # TODO - VTIMEZONE
82
+ }
83
+
84
+ inner.each do |component|
85
+ name = component.first.value
86
+
87
+ if klass = factory[name]
88
+ @components << klass.new(component)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Add an event to this calendar.
94
+ #
95
+ # Yields an event maker, Icalendar::Vevent::Maker.
96
+ def add_event(&block) #:yield:event
97
+ push Vevent::Maker.make( &block )
98
+ end
99
+
100
+ # TODO add_todo, add_journal
101
+
102
+ =begin
103
+ TODO
104
+ # Allows customization of calendar creation.
105
+ class Maker
106
+ def initialize #:nodoc:
107
+ @prodid = Vpim::PRODID
108
+ end
109
+
110
+ attr :prodid
111
+ end
112
+ =end
113
+
114
+ # The producer ID defaults to Vpim::PRODID but you can set it to something
115
+ # specific to your application.
116
+ def Icalendar.create2(producer = Vpim::PRODID, tzinfo_id = nil) #:yield: self
117
+ # FIXME - make the primary API
118
+ di = DirectoryInfo.create( [ DirectoryInfo::Field.create('VERSION', '2.0') ], 'VCALENDAR' )
119
+
120
+ di.push_unique DirectoryInfo::Field.create('PRODID', producer.to_str)
121
+ di.push_unique DirectoryInfo::Field.create('X-WR-TIMEZONE', tzinfo_id) unless tzinfo_id.nil?
122
+ di.push_unique DirectoryInfo::Field.create('CALSCALE', "Gregorian")
123
+
124
+ cal = new(di.to_a)
125
+
126
+ if block_given?
127
+ yield cal
128
+ end
129
+
130
+ cal
131
+ end
132
+
133
+ # Create a new Icalendar object with the minimal set of fields for a valid
134
+ # Calendar. If specified, +fields+ must be an array of
135
+ # DirectoryInfo::Field objects to add. They can override the the default
136
+ # Calendar fields, so, for example, this can be used to set a custom PRODID field.
137
+ def Icalendar.create(fields=[])
138
+ di = DirectoryInfo.create( [ DirectoryInfo::Field.create('VERSION', '2.0') ], 'VCALENDAR' )
139
+
140
+ DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
141
+
142
+ di.push_unique DirectoryInfo::Field.create('PRODID', Vpim::PRODID)
143
+ di.push_unique DirectoryInfo::Field.create('CALSCALE', "Gregorian")
144
+
145
+ new(di.to_a)
146
+ end
147
+
148
+ # Create a new Icalendar object with a protocol method of REPLY.
149
+ #
150
+ # Meeting requests, and such, are Calendar containers with a protocol
151
+ # method of REQUEST, and contains some number of Events, Todos, etc.,
152
+ # that may need replying to. In order to reply to any of these components
153
+ # of a request, you must first build a Calendar object to hold your reply
154
+ # components.
155
+ #
156
+ # This method builds the reply Calendar, you then will add to it replies
157
+ # to the specific components of the request Calendar that you are replying
158
+ # to. If you have any particular fields that you want to be in the
159
+ # Calendar, other than the defaults, then can be supplied as +fields+, an
160
+ # array of Field objects.
161
+ def Icalendar.create_reply(fields=[])
162
+ fields << DirectoryInfo::Field.create('METHOD', 'REPLY')
163
+
164
+ Icalendar.create(fields)
165
+ end
166
+
167
+ # Used during encoding.
168
+ def fields # :nodoc:
169
+ f = @properties.to_a
170
+ last = f.pop
171
+ # Use of #each means we won't encode components in our View, but also
172
+ # that we won't encode timezones... but we don't decode/support timezones
173
+ # anyhow, so fix later.
174
+ each { |c| f << c.fields }
175
+ f.push last
176
+ end
177
+
178
+ # Encode the Calendar as a string. The width is the maximum width of the
179
+ # encoded lines, it can be specified, but is better left to the default.
180
+ def encode(width=nil)
181
+ # We concatenate the fields of all objects, create a DirInfo, then
182
+ # encode it.
183
+ di = DirectoryInfo.create(self.fields.flatten)
184
+ di.encode(width)
185
+ end
186
+
187
+ alias to_s encode
188
+
189
+ # Push a calendar component onto the calendar.
190
+ def push(component)
191
+ case component
192
+ when Vevent, Vtodo, Vjournal
193
+ @components << component
194
+ else
195
+ raise ArgumentError, "can't add a #{component.type} to a calendar"
196
+ end
197
+ self
198
+ end
199
+
200
+ alias :<< :push
201
+
202
+ # Check if the protocol method is +method+
203
+ def protocol?(method)
204
+ Vpim::Methods.casecmp?(protocol, method)
205
+ end
206
+
207
+ def Icalendar.decode_duration(str) #:nodoc:
208
+ unless match = %r{\s*#{Bnf::DURATION}\s*}.match(str)
209
+ raise InvalidEncodingError, "duration not valid (#{str})"
210
+ end
211
+ dur = 0
212
+
213
+ # Remember: match[0] is the whole match string, match[1] is $1, etc.
214
+
215
+ # Week
216
+ if match[2]
217
+ dur = match[2].to_i
218
+ end
219
+ # Days
220
+ dur *= 7
221
+ if match[3]
222
+ dur += match[3].to_i
223
+ end
224
+ # Hours
225
+ dur *= 24
226
+ if match[4]
227
+ dur += match[4].to_i
228
+ end
229
+ # Minutes
230
+ dur *= 60
231
+ if match[5]
232
+ dur += match[5].to_i
233
+ end
234
+ # Seconds
235
+ dur *= 60
236
+ if match[6]
237
+ dur += match[6].to_i
238
+ end
239
+
240
+ if match[1] && match[1] == '-'
241
+ dur = -dur
242
+ end
243
+
244
+ dur
245
+ end
246
+
247
+ # Decode iCalendar data into an array of Icalendar objects.
248
+ #
249
+ # Since iCalendars are self-delimited (by a BEGIN:VCALENDAR and an
250
+ # END:VCALENDAR), multiple iCalendars can be concatenated into a single
251
+ # file.
252
+ #
253
+ # cal must be String or IO, or implement #each by returning
254
+ # each line in the input as those classes do.
255
+ def Icalendar.decode(cal, e = nil)
256
+ entities = Vpim.expand(Vpim.decode(cal))
257
+
258
+ # Since all iCalendars must have a begin/end, the top-level should
259
+ # consist entirely of entities/arrays, even if its a single iCalendar.
260
+ if entities.detect { |e| ! e.kind_of? Array }
261
+ raise "Not a valid iCalendar"
262
+ end
263
+
264
+ calendars = []
265
+
266
+ entities.each do |e|
267
+ calendars << new(e)
268
+ end
269
+
270
+ calendars
271
+ end
272
+
273
+ # The iCalendar version multiplied by 10 as an Integer. iCalendar must have
274
+ # a version of 20, and vCalendar must have a version of 10.
275
+ def version
276
+ v = @properties['VERSION']
277
+
278
+ unless v
279
+ raise InvalidEncodingError, "Invalid calendar, no version field!"
280
+ end
281
+
282
+ v = v.to_f * 10
283
+ v = v.to_i
284
+ end
285
+
286
+ # The value of the PRODID field, an unstructured string meant to
287
+ # identify the software which encoded the Calendar data.
288
+ def producer
289
+ #f = @properties.field('PRODID')
290
+ #f && f.to_text
291
+ @properties.text('PRODID').first
292
+ end
293
+
294
+ # The value of the METHOD field. Protocol methods are used when iCalendars
295
+ # are exchanged in a calendar messaging system, such as iTIP or iMIP. When
296
+ # METHOD is not specified, the Calendar object is merely being used to
297
+ # transport a snapshot of some calendar information; without the intention
298
+ # of conveying a scheduling semantic.
299
+ #
300
+ # Note that this method can't be called +method+, thats already a method of
301
+ # Object.
302
+ def protocol
303
+ m = @properties['METHOD']
304
+ m ? m.upcase : m
305
+ end
306
+
307
+ # The value of the CALSCALE: property, or "GREGORIAN" if CALSCALE: is not
308
+ # present.
309
+ #
310
+ # This is of academic interest only. There aren't any other calendar scales
311
+ # defined, and given that its hard enough just dealing with Gregorian
312
+ # calendars, there probably won't be.
313
+ def calscale
314
+ (@properties['CALSCALE'] || 'GREGORIAN').upcase
315
+ end
316
+
317
+ # The array of all supported calendar components. If a class is provided,
318
+ # return only the components of that class.
319
+ #
320
+ # If a block is provided, yield the components instead of returning them.
321
+ #
322
+ # Examples:
323
+ # calendar.components(Vpim::Icalendar::Vevent)
324
+ # => array of all calendar components
325
+ #
326
+ # calendar.components(Vpim::Icalendar::Vtodo) {|c| c... }
327
+ # => yield all todo components
328
+ #
329
+ # calendar.components {|c| c... }
330
+ # => yield all components
331
+ #
332
+ # Note - use of this is mildly deprecated in favour of #each, #events,
333
+ # #todos, #journals because those won't return timezones, and will return
334
+ # Enumerators if called without a block.
335
+ def components(klass=Object) #:yields:component
336
+ klass ||= Object
337
+
338
+ unless block_given?
339
+ return @components.select{|c| klass === c}.freeze
340
+ end
341
+
342
+ @components.each do |c|
343
+ if klass === c
344
+ yield c
345
+ end
346
+ end
347
+ self
348
+ end
349
+
350
+ include Enumerable
351
+
352
+ # Enumerate the top-level calendar components. Yields them if a block
353
+ # is provided, otherwise returns an Enumerator.
354
+ #
355
+ # This skips components that are only internally meaningful to iCalendar,
356
+ # such as timezone definitions.
357
+ def each(klass=nil, &block) # :yield: component
358
+ unless block
359
+ return Enumerable::Enumerator.new(self, :each, klass)
360
+ end
361
+ components(klass, &block)
362
+ end
363
+
364
+ # Short-hand for #each(Icalendar::Vevent).
365
+ def events(&block) #:yield: Vevent
366
+ each(Icalendar::Vevent, &block)
367
+ end
368
+
369
+ # Short-hand for #each(Icalendar::Vtodo).
370
+ def todos(&block) #:yield: Vtodo
371
+ each(Icalendar::Vtodo, &block)
372
+ end
373
+
374
+ # Short-hand for #each(Icalendar::Vjournal).
375
+ def journals(&block) #:yield: Vjournal
376
+ each(Icalendar::Vjournal, &block)
377
+ end
378
+
379
+ end
380
+
381
+ end
382
+
@@ -0,0 +1,16 @@
1
+ =begin
2
+ Copyright (C) 2008 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/vcard'
10
+
11
+ module Vpim
12
+ module Maker #:nodoc:backwards compat
13
+ Vcard = Vpim::Vcard::Maker #:nodoc:backwards compat
14
+ end
15
+ end
16
+
@@ -0,0 +1,193 @@
1
+ =begin
2
+ Copyright (C) 2008 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
+ module Vpim
10
+ class Icalendar
11
+ module Set #:nodoc:
12
+ module Util #:nodoc:
13
+ # TODO - rename module to Private?
14
+
15
+ def rm_all(name)
16
+ rm = @comp.properties.select { |f| f.name? name }
17
+ rm.each { |f| @comp.properties.delete(f) }
18
+ end
19
+
20
+ def set_token(name, allowed, default, value) #:nodoc:
21
+ value = value.to_str
22
+ unless allowed.include?(value)
23
+ raise Vpim::Unencodeable, "Invalid #{name} value '#{value}'"
24
+ end
25
+ rm_all(name)
26
+ unless value == default
27
+ @comp.properties.push Vpim::DirectoryInfo::Field.create(name, value)
28
+ end
29
+ end
30
+
31
+ def field_create(name, value, default_value_type = nil, value_type = nil, params = {})
32
+ if value_type && value_type != default_value_type
33
+ params['VALUE'] = value_type
34
+ end
35
+ Vpim::DirectoryInfo::Field.create(name, value, params)
36
+ end
37
+
38
+ def set_date_or_datetime(name, default, value)
39
+ f = nil
40
+ case value
41
+ when Date
42
+ f = field_create(name, Vpim.encode_date(value), default, 'DATE')
43
+ when Time
44
+ f = field_create(name, Vpim.encode_date_time(value), default, 'DATE-TIME')
45
+ else
46
+ raise Vpim::Unencodeable, "Invalid #{name} value #{value.inspect}"
47
+ end
48
+ rm_all(name)
49
+ @comp.properties.push(f)
50
+ end
51
+
52
+ def set_datetime(name, value)
53
+ f = field_create(name, Vpim.encode_date_time(value))
54
+ rm_all(name)
55
+ @comp.properties.push(f)
56
+ end
57
+
58
+ def set_text(name, value)
59
+ f = field_create(name, Vpim.encode_text(value))
60
+ rm_all(name)
61
+ @comp.properties.push(f)
62
+ end
63
+
64
+ def set_text_list(name, value)
65
+ f = field_create(name, Vpim.encode_text_list(value))
66
+ rm_all(name)
67
+ @comp.properties.push(f)
68
+ end
69
+
70
+ def set_integer(name, value)
71
+ value = value.to_int.to_s
72
+ f = field_create(name, value)
73
+ rm_all(name)
74
+ @comp.properties.push(f)
75
+ end
76
+
77
+ def add_address(name, value)
78
+ f = value.encode(name)
79
+ @comp.properties.push(f)
80
+ end
81
+
82
+ def set_address(name, value)
83
+ rm_all(name)
84
+ add_address(name, value)
85
+ end
86
+
87
+ end
88
+ end
89
+
90
+ module Property #:nodoc:
91
+
92
+ # FIXME - rename Base to Util
93
+ module Base #:nodoc:
94
+ # Value of first property with name +name+
95
+ def propvalue(name) #:nodoc:
96
+ prop = @properties.detect { |f| f.name? name }
97
+ if prop
98
+ prop = prop.value
99
+ end
100
+ prop
101
+ end
102
+
103
+ # Array of values of all properties with name +name+
104
+ def propvaluearray(name) #:nodoc:
105
+ @properties.select{ |f| f.name? name }.map{ |p| p.value }
106
+ end
107
+
108
+
109
+ def propinteger(name) #:nodoc:
110
+ prop = @properties.detect { |f| f.name? name }
111
+ if prop
112
+ prop = Vpim.decode_integer(prop.value)
113
+ end
114
+ prop
115
+ end
116
+
117
+ def proptoken(name, allowed, default_token = nil) #:nodoc:
118
+ prop = propvalue(name)
119
+
120
+ if prop
121
+ prop = prop.to_str.upcase
122
+ unless allowed.include?(prop)
123
+ raise Vpim::InvalidEncodingError, "Invalid #{name} value '#{prop}'"
124
+ end
125
+ else
126
+ prop = default_token
127
+ end
128
+
129
+ prop
130
+ end
131
+
132
+ # Value as DATE-TIME or DATE of object of first property with name +name+
133
+ def proptime(name) #:nodoc:
134
+ prop = @properties.detect { |f| f.name? name }
135
+ if prop
136
+ prop = prop.to_time.first
137
+ end
138
+ prop
139
+ end
140
+
141
+ # Value as TEXT of first property with name +name+
142
+ def proptext(name) #:nodoc:
143
+ prop = @properties.detect { |f| f.name? name }
144
+ if prop
145
+ prop = prop.to_text
146
+ end
147
+ prop
148
+ end
149
+
150
+ # Array of values as TEXT of all properties with name +name+
151
+ def proptextarray(name) #:nodoc:
152
+ @properties.select{ |f| f.name? name }.map{ |p| p.to_text }
153
+ end
154
+
155
+ # Array of values as TEXT list of all properties with name +name+
156
+ def proptextlistarray(name) #:nodoc:
157
+ @properties.select{ |f| f.name? name }.map{ |p| Vpim.decode_text_list(p.value_raw) }.flatten
158
+ end
159
+
160
+ # Duration has "almost" the same definition for Event and Todo
161
+ def propduration(endfield)
162
+ dur = @properties.field 'DURATION'
163
+ dte = @properties.field endfield
164
+
165
+ if !dur
166
+ return nil unless dte
167
+
168
+ b = dtstart
169
+ e = send(endfield.downcase.to_sym)
170
+
171
+ return (e - b).to_i
172
+ end
173
+
174
+ Icalendar.decode_duration(dur.value_raw)
175
+ end
176
+
177
+ def propend(endfield)
178
+ dte = @properties.field endfield.to_s.upcase
179
+ if dte
180
+ dte.to_time.first
181
+ elsif duration
182
+ dtstart + duration
183
+ else
184
+ nil
185
+ end
186
+ end
187
+
188
+
189
+ end
190
+ end
191
+ end
192
+ end
193
+