mumboe-vpim 0.7

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