vpim2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES +504 -0
  3. data/COPYING +58 -0
  4. data/README +182 -0
  5. data/lib/atom.rb +728 -0
  6. data/lib/plist.rb +22 -0
  7. data/lib/vpim.rb +13 -0
  8. data/lib/vpim/address.rb +219 -0
  9. data/lib/vpim/attachment.rb +102 -0
  10. data/lib/vpim/date.rb +222 -0
  11. data/lib/vpim/dirinfo.rb +277 -0
  12. data/lib/vpim/duration.rb +119 -0
  13. data/lib/vpim/enumerator.rb +32 -0
  14. data/lib/vpim/field.rb +614 -0
  15. data/lib/vpim/icalendar.rb +381 -0
  16. data/lib/vpim/maker/vcard.rb +16 -0
  17. data/lib/vpim/property/base.rb +193 -0
  18. data/lib/vpim/property/common.rb +315 -0
  19. data/lib/vpim/property/location.rb +38 -0
  20. data/lib/vpim/property/priority.rb +43 -0
  21. data/lib/vpim/property/recurrence.rb +69 -0
  22. data/lib/vpim/property/resources.rb +24 -0
  23. data/lib/vpim/repo.rb +181 -0
  24. data/lib/vpim/rfc2425.rb +367 -0
  25. data/lib/vpim/rrule.rb +591 -0
  26. data/lib/vpim/vcard.rb +1430 -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 +117 -0
@@ -0,0 +1,381 @@
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) #: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('CALSCALE', "Gregorian")
122
+
123
+ cal = new(di.to_a)
124
+
125
+ if block_given?
126
+ yield cal
127
+ end
128
+
129
+ cal
130
+ end
131
+
132
+ # Create a new Icalendar object with the minimal set of fields for a valid
133
+ # Calendar. If specified, +fields+ must be an array of
134
+ # DirectoryInfo::Field objects to add. They can override the the default
135
+ # Calendar fields, so, for example, this can be used to set a custom PRODID field.
136
+ def Icalendar.create(fields=[])
137
+ di = DirectoryInfo.create( [ DirectoryInfo::Field.create('VERSION', '2.0') ], 'VCALENDAR' )
138
+
139
+ DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
140
+
141
+ di.push_unique DirectoryInfo::Field.create('PRODID', Vpim::PRODID)
142
+ di.push_unique DirectoryInfo::Field.create('CALSCALE', "Gregorian")
143
+
144
+ new(di.to_a)
145
+ end
146
+
147
+ # Create a new Icalendar object with a protocol method of REPLY.
148
+ #
149
+ # Meeting requests, and such, are Calendar containers with a protocol
150
+ # method of REQUEST, and contains some number of Events, Todos, etc.,
151
+ # that may need replying to. In order to reply to any of these components
152
+ # of a request, you must first build a Calendar object to hold your reply
153
+ # components.
154
+ #
155
+ # This method builds the reply Calendar, you then will add to it replies
156
+ # to the specific components of the request Calendar that you are replying
157
+ # to. If you have any particular fields that you want to be in the
158
+ # Calendar, other than the defaults, then can be supplied as +fields+, an
159
+ # array of Field objects.
160
+ def Icalendar.create_reply(fields=[])
161
+ fields << DirectoryInfo::Field.create('METHOD', 'REPLY')
162
+
163
+ Icalendar.create(fields)
164
+ end
165
+
166
+ # Used during encoding.
167
+ def fields # :nodoc:
168
+ f = @properties.to_a
169
+ last = f.pop
170
+ # Use of #each means we won't encode components in our View, but also
171
+ # that we won't encode timezones... but we don't decode/support timezones
172
+ # anyhow, so fix later.
173
+ each { |c| f << c.fields }
174
+ f.push last
175
+ end
176
+
177
+ # Encode the Calendar as a string. The width is the maximum width of the
178
+ # encoded lines, it can be specified, but is better left to the default.
179
+ def encode(width=nil)
180
+ # We concatenate the fields of all objects, create a DirInfo, then
181
+ # encode it.
182
+ di = DirectoryInfo.create(self.fields.flatten)
183
+ di.encode(width)
184
+ end
185
+
186
+ alias to_s encode
187
+
188
+ # Push a calendar component onto the calendar.
189
+ def push(component)
190
+ case component
191
+ when Vevent, Vtodo, Vjournal
192
+ @components << component
193
+ else
194
+ raise ArgumentError, "can't add a #{component.type} to a calendar"
195
+ end
196
+ self
197
+ end
198
+
199
+ alias :<< :push
200
+
201
+ # Check if the protocol method is +method+
202
+ def protocol?(method)
203
+ Vpim::Methods.casecmp?(protocol, method)
204
+ end
205
+
206
+ def Icalendar.decode_duration(str) #:nodoc:
207
+ unless match = %r{\s*#{Bnf::DURATION}\s*}.match(str)
208
+ raise InvalidEncodingError, "duration not valid (#{str})"
209
+ end
210
+ dur = 0
211
+
212
+ # Remember: match[0] is the whole match string, match[1] is $1, etc.
213
+
214
+ # Week
215
+ if match[2]
216
+ dur = match[2].to_i
217
+ end
218
+ # Days
219
+ dur *= 7
220
+ if match[3]
221
+ dur += match[3].to_i
222
+ end
223
+ # Hours
224
+ dur *= 24
225
+ if match[4]
226
+ dur += match[4].to_i
227
+ end
228
+ # Minutes
229
+ dur *= 60
230
+ if match[5]
231
+ dur += match[5].to_i
232
+ end
233
+ # Seconds
234
+ dur *= 60
235
+ if match[6]
236
+ dur += match[6].to_i
237
+ end
238
+
239
+ if match[1] && match[1] == '-'
240
+ dur = -dur
241
+ end
242
+
243
+ dur
244
+ end
245
+
246
+ # Decode iCalendar data into an array of Icalendar objects.
247
+ #
248
+ # Since iCalendars are self-delimited (by a BEGIN:VCALENDAR and an
249
+ # END:VCALENDAR), multiple iCalendars can be concatenated into a single
250
+ # file.
251
+ #
252
+ # cal must be String or IO, or implement #each by returning
253
+ # each line in the input as those classes do.
254
+ def Icalendar.decode(cal, e = nil)
255
+ entities = Vpim.expand(Vpim.decode(cal))
256
+
257
+ # Since all iCalendars must have a begin/end, the top-level should
258
+ # consist entirely of entities/arrays, even if its a single iCalendar.
259
+ if entities.detect { |e| ! e.kind_of? Array }
260
+ raise "Not a valid iCalendar"
261
+ end
262
+
263
+ calendars = []
264
+
265
+ entities.each do |e|
266
+ calendars << new(e)
267
+ end
268
+
269
+ calendars
270
+ end
271
+
272
+ # The iCalendar version multiplied by 10 as an Integer. iCalendar must have
273
+ # a version of 20, and vCalendar must have a version of 10.
274
+ def version
275
+ v = @properties['VERSION']
276
+
277
+ unless v
278
+ raise InvalidEncodingError, "Invalid calendar, no version field!"
279
+ end
280
+
281
+ v = v.to_f * 10
282
+ v = v.to_i
283
+ end
284
+
285
+ # The value of the PRODID field, an unstructured string meant to
286
+ # identify the software which encoded the Calendar data.
287
+ def producer
288
+ #f = @properties.field('PRODID')
289
+ #f && f.to_text
290
+ @properties.text('PRODID').first
291
+ end
292
+
293
+ # The value of the METHOD field. Protocol methods are used when iCalendars
294
+ # are exchanged in a calendar messaging system, such as iTIP or iMIP. When
295
+ # METHOD is not specified, the Calendar object is merely being used to
296
+ # transport a snapshot of some calendar information; without the intention
297
+ # of conveying a scheduling semantic.
298
+ #
299
+ # Note that this method can't be called +method+, thats already a method of
300
+ # Object.
301
+ def protocol
302
+ m = @properties['METHOD']
303
+ m ? m.upcase : m
304
+ end
305
+
306
+ # The value of the CALSCALE: property, or "GREGORIAN" if CALSCALE: is not
307
+ # present.
308
+ #
309
+ # This is of academic interest only. There aren't any other calendar scales
310
+ # defined, and given that its hard enough just dealing with Gregorian
311
+ # calendars, there probably won't be.
312
+ def calscale
313
+ (@properties['CALSCALE'] || 'GREGORIAN').upcase
314
+ end
315
+
316
+ # The array of all supported calendar components. If a class is provided,
317
+ # return only the components of that class.
318
+ #
319
+ # If a block is provided, yield the components instead of returning them.
320
+ #
321
+ # Examples:
322
+ # calendar.components(Vpim::Icalendar::Vevent)
323
+ # => array of all calendar components
324
+ #
325
+ # calendar.components(Vpim::Icalendar::Vtodo) {|c| c... }
326
+ # => yield all todo components
327
+ #
328
+ # calendar.components {|c| c... }
329
+ # => yield all components
330
+ #
331
+ # Note - use of this is mildly deprecated in favour of #each, #events,
332
+ # #todos, #journals because those won't return timezones, and will return
333
+ # Enumerators if called without a block.
334
+ def components(klass=Object) #:yields:component
335
+ klass ||= Object
336
+
337
+ unless block_given?
338
+ return @components.select{|c| klass === c}.freeze
339
+ end
340
+
341
+ @components.each do |c|
342
+ if klass === c
343
+ yield c
344
+ end
345
+ end
346
+ self
347
+ end
348
+
349
+ include Enumerable
350
+
351
+ # Enumerate the top-level calendar components. Yields them if a block
352
+ # is provided, otherwise returns an Enumerator.
353
+ #
354
+ # This skips components that are only internally meaningful to iCalendar,
355
+ # such as timezone definitions.
356
+ def each(klass=nil, &block) # :yield: component
357
+ unless block
358
+ return Enumerable::Enumerator.new(self, :each, klass)
359
+ end
360
+ components(klass, &block)
361
+ end
362
+
363
+ # Short-hand for #each(Icalendar::Vevent).
364
+ def events(&block) #:yield: Vevent
365
+ each(Icalendar::Vevent, &block)
366
+ end
367
+
368
+ # Short-hand for #each(Icalendar::Vtodo).
369
+ def todos(&block) #:yield: Vtodo
370
+ each(Icalendar::Vtodo, &block)
371
+ end
372
+
373
+ # Short-hand for #each(Icalendar::Vjournal).
374
+ def journals(&block) #:yield: Vjournal
375
+ each(Icalendar::Vjournal, &block)
376
+ end
377
+
378
+ end
379
+
380
+ end
381
+
@@ -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
+