mumboe-vpim 0.7

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