vpim2 0.0.1

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 (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
data/lib/plist.rb ADDED
@@ -0,0 +1,22 @@
1
+ #--
2
+ ##############################################################
3
+ # Copyright 2006, Ben Bleything <ben@bleything.net> and #
4
+ # Patrick May <patrick@hexane.org> #
5
+ # #
6
+ # Distributed under the MIT license. #
7
+ ##############################################################
8
+ #++
9
+ # = Plist
10
+ #
11
+ # This is the main file for plist. Everything interesting happens in Plist and Plist::Emit.
12
+
13
+ require 'base64'
14
+ require 'cgi'
15
+ require 'stringio'
16
+
17
+ require 'plist/generator'
18
+ require 'plist/parser'
19
+
20
+ module Plist
21
+ VERSION = '3.0.0'
22
+ end
data/lib/vpim.rb ADDED
@@ -0,0 +1,13 @@
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
+ # the existence of this file is a hack to support users or rubygems
10
+
11
+ require 'vpim/icalendar'
12
+ require 'vpim/vcard'
13
+
@@ -0,0 +1,219 @@
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
+ =begin
10
+
11
+ Notes on a CAL-ADDRESS
12
+
13
+ When used with ATTENDEE, the parameters are:
14
+ CN
15
+ CUTYPE
16
+ DELEGATED-FROM
17
+ DELEGATED-TO
18
+ DIR
19
+ LANGUAGE
20
+ MEMBER
21
+ PARTSTAT
22
+ ROLE
23
+ RSVP
24
+ SENT-BY
25
+
26
+ When used with ORGANIZER, the parameters are:
27
+ CN
28
+ DIR
29
+ LANGUAGE
30
+ SENT-BY
31
+
32
+
33
+ What I've seen in Notes invitations, and iCal responses:
34
+ ROLE
35
+ PARTSTAT
36
+ RSVP
37
+ CN
38
+
39
+ Support these last 4, for now.
40
+
41
+ =end
42
+
43
+ module Vpim
44
+ class Icalendar
45
+ # Used to represent calendar fields containing CAL-ADDRESS values.
46
+ # The organizer or the attendees of a calendar event are examples of such
47
+ # a field.
48
+ #
49
+ # Example:
50
+ #
51
+ # ORGANIZER;CN="A. Person":mailto:a_person@example.com
52
+ #
53
+ # ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION
54
+ # ;CN="Sam Roberts";RSVP=TRUE:mailto:SRoberts@example.com
55
+ #
56
+ class Address
57
+
58
+ # Create a copy of Address. If the original Address was frozen, this one
59
+ # won't be.
60
+ def copy
61
+ #Marshal.load(Marshal.dump(self))
62
+ self.dup.dirty
63
+ end
64
+
65
+ def dirty #:nodoc:
66
+ @field = nil
67
+ self
68
+ end
69
+
70
+ # Addresses in a CAL-ADDRESS are represented as a URI, usually a mailto URI.
71
+ attr_accessor :uri
72
+ # The common or displayable name associated with the calendar address, or
73
+ # nil if there is none.
74
+ attr_accessor :cn
75
+ # The participation role for the calendar user specified by the address.
76
+ #
77
+ # The standard roles are:
78
+ # - CHAIR Indicates chair of the calendar entity
79
+ # - REQ-PARTICIPANT Indicates a participant whose participation is required
80
+ # - OPT-PARTICIPANT Indicates a participant whose participation is optional
81
+ # - NON-PARTICIPANT Indicates a participant who is copied for information purposes only
82
+ #
83
+ # The default role is REQ-PARTICIPANT, returned if no ROLE parameter was
84
+ # specified.
85
+ attr_accessor :role
86
+ # The participation status for the calendar user specified by the
87
+ # property PARTSTAT, a String.
88
+ #
89
+ # These are the participation statuses for an Event:
90
+ # - NEEDS-ACTION Event needs action
91
+ # - ACCEPTED Event accepted
92
+ # - DECLINED Event declined
93
+ # - TENTATIVE Event tentatively accepted
94
+ # - DELEGATED Event delegated
95
+ #
96
+ # Default is NEEDS-ACTION.
97
+ #
98
+ # FIXME - make the default depend on the component type.
99
+ attr_accessor :partstat
100
+ # The value of the RSVP field, either +true+ or +false+. It is used to
101
+ # specify whether there is an expectation of a reply from the calendar
102
+ # user specified by the property value.
103
+ attr_accessor :rsvp
104
+
105
+ def initialize(field=nil) #:nodoc:
106
+ @field = field
107
+ @uri = ''
108
+ @cn = ''
109
+ @role = "REQ-PARTICIPANT"
110
+ @partstat = "NEEDS-ACTION"
111
+ @rsvp = false
112
+ end
113
+
114
+ # Create a new Address. It will encode as a +name+ property.
115
+ def self.create(uri='')
116
+ adr = new
117
+ adr.uri = uri.to_str
118
+ adr
119
+ end
120
+
121
+ def self.decode(field)
122
+ adr = new(field)
123
+ adr.uri = field.value
124
+
125
+ cn = field.param('CN')
126
+
127
+ if cn
128
+ adr.cn = cn.first
129
+ end
130
+
131
+ role = field.param('ROLE')
132
+
133
+ if role
134
+ adr.role = role.first.strip.upcase
135
+ end
136
+
137
+ partstat = field.param('PARTSTAT')
138
+
139
+ if partstat
140
+ adr.partstat = partstat.first.strip.upcase
141
+ end
142
+
143
+ rsvp = field.param('RSVP')
144
+
145
+ if rsvp
146
+ adr.rsvp = case rsvp.first
147
+ when /TRUE/i then true
148
+ when /FALSE/i then false
149
+ else raise InvalidEncodingError, "RSVP param value not TRUE/FALSE: #{rsvp}"
150
+ end
151
+ end
152
+
153
+ adr.freeze
154
+ end
155
+
156
+ # Return a representation of this Address as a DirectoryInfo::Field.
157
+ def encode(name) #:nodoc:
158
+ if @field
159
+ # FIXME - set the field name, it could be different from cached
160
+ return @field
161
+ end
162
+
163
+ value = uri.to_str.strip
164
+
165
+ if value.empty?
166
+ raise Uencodeable, "Address#uri is zero-length"
167
+ end
168
+
169
+ params = {}
170
+
171
+ if cn.length > 0
172
+ params['CN'] = Vpim::encode_paramvalue(cn)
173
+ end
174
+
175
+ # FIXME - default value is different for non-vEvent
176
+ if role.length > 0 && role != 'REQ-PARTICIPANT'
177
+ params['ROLE'] = Vpim::encode_paramtext(role)
178
+ end
179
+
180
+ # FIXME - default value is different for non-vEvent
181
+ if partstat.length > 0 && partstat != 'NEEDS-ACTION'
182
+ params['PARTSTAT'] = Vpim::encode_paramtext(partstat)
183
+ end
184
+
185
+ if rsvp
186
+ params['RSVP'] = 'true'
187
+ end
188
+
189
+ Vpim::DirectoryInfo::Field.create(name, value, params)
190
+ end
191
+
192
+ # Return true if the +uri+ is == to this address' URI. The comparison
193
+ # is case-insensitive (because email addresses and domain names are).
194
+ def ==(uri)
195
+ # TODO - could I use a URI library?
196
+ Vpim::Methods.casecmp?(self.uri.to_str, uri.to_str)
197
+ end
198
+
199
+ # A string representation of an address, using the common name, and the
200
+ # URI. The URI protocol is stripped if it's "mailto:".
201
+ def to_s
202
+ u = uri
203
+ u = u.gsub(/^mailto: */i, '')
204
+
205
+ if cn.length > 0
206
+ "#{cn.inspect} <#{uri}>"
207
+ else
208
+ uri
209
+ end
210
+ end
211
+
212
+ def inspect #:nodoc:
213
+ "#<Vpim::Icalendar::Address:cn=#{cn.inspect} status=#{partstat} rsvp=#{rsvp} #{uri.inspect}>"
214
+ end
215
+
216
+ end
217
+ end
218
+ end
219
+
@@ -0,0 +1,102 @@
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/icalendar'
10
+
11
+ module Vpim
12
+
13
+ # Attachments are used by both iCalendar and vCard. They are either a URI or
14
+ # inline data, and their decoded value will be either a Uri or a Inline, as
15
+ # appropriate.
16
+ #
17
+ # Besides the methods specific to their class, both kinds of object implement
18
+ # a set of common methods, allowing them to be treated uniformly:
19
+ # - Uri#to_io, Inline#to_io: return an IO from which the value can be read.
20
+ # - Uri#to_s, Inline#to_s: return the value as a String.
21
+ # - Uri#format, Inline#format: the format of the value. This is supposed to
22
+ # be an "iana defined" identifier (like "image/jpeg"), but could be almost
23
+ # anything (or nothing) in practice. Since the parameter is optional, it may
24
+ # be "".
25
+ #
26
+ # The objects can also be distinguished by their class, if necessary.
27
+ module Attachment
28
+
29
+ # TODO - It might be possible to autodetect the format from the first few
30
+ # bytes of the value, and return the appropriate MIME type when format
31
+ # isn't defined.
32
+ #
33
+ # iCalendar and vCard put the format in different parameters, and the
34
+ # default kind of value is different.
35
+ def Attachment.decode(field, defkind, fmtparam) #:nodoc:
36
+ format = field.pvalue(fmtparam) || ''
37
+ kind = field.kind || defkind
38
+ case kind
39
+ when 'text'
40
+ Inline.new(Vpim.decode_text(field.value), format)
41
+ when 'uri'
42
+ Uri.new(field.value_raw, format)
43
+ when 'binary'
44
+ Inline.new(field.value, format)
45
+ else
46
+ raise InvalidEncodingError, "Attachment of type #{kind} is not allowed"
47
+ end
48
+ end
49
+
50
+ # Extends a String to support some of the same methods as Uri.
51
+ class Inline < String
52
+ def initialize(s, format) #:nodoc:
53
+ @format = format
54
+ super(s)
55
+ end
56
+
57
+ # Return an IO object for the inline data. See +stringio+ for more
58
+ # information.
59
+ def to_io
60
+ StringIO.new(self)
61
+ end
62
+
63
+ # The format of the inline data.
64
+ # See Attachment.
65
+ attr_reader :format
66
+ end
67
+
68
+ # Encapsulates a URI and implements some methods of String.
69
+ class Uri
70
+ def initialize(uri, format) #:nodoc:
71
+ @uri = uri
72
+ @format = format
73
+ end
74
+
75
+ # The URI value.
76
+ attr_reader :uri
77
+
78
+ # The format of the data referred to by the URI.
79
+ # See Attachment.
80
+ attr_reader :format
81
+
82
+ # Return an IO object from opening the URI. See +open-uri+ for more
83
+ # information.
84
+ def to_io
85
+ open(@uri)
86
+ end
87
+
88
+ # Return the String from reading the IO object to end-of-data.
89
+ def to_s
90
+ to_io.read(nil)
91
+ end
92
+
93
+ def inspect #:nodoc:
94
+ s = "<#{self.class.to_s}: #{uri.inspect}>"
95
+ s << ", #{@format.inspect}" if @format
96
+ s
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+
data/lib/vpim/date.rb ADDED
@@ -0,0 +1,222 @@
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 'date'
10
+
11
+ # Extensions to the standard library Date.
12
+ class Date
13
+
14
+ TIME_START = Date.new(1970, 1, 1)
15
+ SECS_PER_DAY = 24 * 60 * 60
16
+
17
+ # Converts this object to a Time object, or throws an ArgumentError if
18
+ # conversion is not possible because it is before the start of epoch.
19
+ def vpim_to_time
20
+ raise ArgumentError, 'date is before the start of system time' if self < TIME_START
21
+ days = self - TIME_START
22
+
23
+ Time.at((days * SECS_PER_DAY).to_i)
24
+ end
25
+
26
+ # If wday responds to to_str, convert it to the wday number by searching for
27
+ # a wday that matches, using as many characters as are in wday to do the
28
+ # comparison. wday must be 2 or more characters long in order to be a unique
29
+ # match, other than that, "mo", "Mon", and "MonDay" are all valid strings
30
+ # for wday 1.
31
+ #
32
+ # This method can be called on a valid wday, and it will return it. Perhaps
33
+ # it should be called by default inside the Date#new*() methods so that
34
+ # non-integer wday arguments can be used? Perhaps a similar method should
35
+ # exist for months? But with months, we all know January is 1, who can
36
+ # remember where Date chooses to start its wday count!
37
+ #
38
+ # Examples:
39
+ # Date.bywday(2004, 2, Date.str2wday('TU')) => the first Tuesday in
40
+ # February
41
+ # Date.bywday(2004, 2, Date.str2wday(2)) => the same day, but notice
42
+ # that a valid wday integer can be passed right through.
43
+ #
44
+ def Date.str2wday(wdaystr)
45
+ return wdaystr unless wdaystr.respond_to? :to_str
46
+
47
+ str = wdaystr.to_str.upcase
48
+ if str.length < 2
49
+ raise ArgumentError, 'wday #{wday} is not long enough to be a unique weekday name'
50
+ end
51
+
52
+ wday = Date::DAYNAMES.map { |n| n.slice(0, str.length).upcase }.index(str)
53
+
54
+ return wday if wday
55
+
56
+ raise ArgumentError, 'wday #{wdaystr} was not a recognizable weekday name'
57
+ end
58
+
59
+
60
+ # Create a new Date object for the date specified by year +year+, month
61
+ # +mon+, and day-of-the-week +wday+.
62
+ #
63
+ # The nth, +n+, occurrence of +wday+ within the period will be generated
64
+ # (+n+ defaults to 1). If +n+ is positive, the nth occurrence from the
65
+ # beginning of the period will be returned, if negative, the nth occurrence
66
+ # from the end of the period will be returned.
67
+ #
68
+ # The period is a year, unless +month+ is non-nil, in which case it is just
69
+ # that month.
70
+ #
71
+ # Examples:
72
+ # - Date.bywday(2004, nil, 1, 9) => the ninth Sunday of 2004
73
+ # - Date.bywday(2004, nil, 1) => the first Sunday of 2004
74
+ # - Date.bywday(2004, nil, 1, -2) => the second last Sunday of 2004
75
+ # - Date.bywday(2004, 12, 1) => the first sunday in the 12th month of 2004
76
+ # - Date.bywday(2004, 2, 2, -1) => last Tuesday in the 2nd month in 2004
77
+ # - Date.bywday(2004, -2, 3, -2) => second last Wednesday in the second last month of 2004
78
+ #
79
+ # Compare this to Date.new, which allows a Date to be created by
80
+ # day-of-the-month, mday, to Date.ordinal, which allows a Date to be created by
81
+ # day-of-the-year, yday, and to Date.commercial, which allows a Date to be created
82
+ # by day-of-the-week, but within a specific week.
83
+ def Date.bywday(year, mon, wday, n = 1, sg=Date::ITALY)
84
+ # Normalize mon to 1-12.
85
+ if mon
86
+ if mon > 12 || mon == 0 || mon < -12
87
+ raise ArgumentError, "mon #{mon} must be 1-12 or negative 1-12"
88
+ end
89
+ if mon < 0
90
+ mon = 13 + mon
91
+ end
92
+ end
93
+ if wday < 0 || wday > 6
94
+ raise ArgumentError, 'wday must be in range 0-6, or a weekday name'
95
+ end
96
+
97
+ # Determine direction of indexing.
98
+ inc = n <=> 0
99
+ if inc == 0
100
+ raise ArgumentError, 'n must be greater or less than zero'
101
+ end
102
+
103
+ # if !mon, n is index into year, but direction of search is determined by
104
+ # sign of n
105
+ d = Date.new(year, mon ? mon : inc, inc, sg)
106
+
107
+ while d.wday != wday
108
+ d += inc
109
+ end
110
+
111
+ # Now we have found the first/last day with the correct wday, search
112
+ # for nth occurrence, by jumping by n.abs-1 weeks forward or backward.
113
+ d += 7 * (n.abs - 1) * inc
114
+
115
+ if d.year != year
116
+ raise ArgumentError, 'n is out of bounds of year'
117
+ end
118
+ if mon && d.mon != mon
119
+ raise ArgumentError, 'n is out of bounds of month'
120
+ end
121
+ d
122
+ end
123
+
124
+ # Return the first day of the week for the specified date. Commercial weeks
125
+ # start on Monday, but the weekstart can be specified (as 0-6, where 0 is
126
+ # sunday, or in formate of Date.str2day).
127
+ def Date.weekstart(year, mon, day, weekstart="MO")
128
+ wkst = Date.str2wday(weekstart)
129
+ d = Date.new(year, mon, day)
130
+ until d.wday == wkst
131
+ d = d - 1
132
+ end
133
+ d
134
+ end
135
+ end
136
+
137
+ # DateGen generates arrays of dates matching simple criteria.
138
+ class DateGen
139
+
140
+ # Generate an array of a week's dates, where week is specified by year, mon,
141
+ # day, and the weekstart (the day-of-week that is considered the "first" day
142
+ # of that week, 0-6, where 0 is sunday).
143
+ def DateGen.weekofdate(year, mon, day, weekstart)
144
+ d = Date.weekstart(year, mon, day, weekstart)
145
+ week = []
146
+ 7.times do
147
+ week << d
148
+ d = d + 1
149
+ end
150
+ week
151
+ end
152
+
153
+ # Generate an array of dates on +wday+ (the day-of-week,
154
+ # 0-6, where 0 is Sunday).
155
+ #
156
+ # If +n+ is specified, only the nth occurrence of +wday+ within the period
157
+ # will be generated. If +n+ is positive, the nth occurrence from the
158
+ # beginning of the period will be returned, if negative, the nth occurrence
159
+ # from the end of the period will be returned.
160
+ #
161
+ # The period is a year, unless +month+ is non-nil, in which case it is just
162
+ # that month.
163
+ #
164
+ # Examples:
165
+ # - DateGen.bywday(2004, nil, 1, 9) => the ninth Sunday in 2004
166
+ # - DateGen.bywday(2004, nil, 1) => all Sundays in 2004
167
+ # - DateGen.bywday(2004, nil, 1, -2) => second last Sunday in 2004
168
+ # - DateGen.bywday(2004, 12, 1) => all sundays in December 2004
169
+ # - DateGen.bywday(2004, 2, 2, -1) => last Tuesday in February in 2004
170
+ # - DateGen.bywday(2004, -2, 3, -2) => second last Wednesday in November of 2004
171
+ #
172
+ # Compare to Date.bywday(), which allows a single Date to be created with
173
+ # similar criteria.
174
+ def DateGen.bywday(year, month, wday, n = nil)
175
+ seed = Date.bywday(year, month, wday, n ? n : 1)
176
+
177
+ dates = [ seed ]
178
+
179
+ return dates if n
180
+
181
+ succ = seed.clone
182
+
183
+ # Collect all matches until we're out of the year (or month, if specified)
184
+ loop do
185
+ succ += 7
186
+
187
+ break if succ.year != year
188
+ break if month && succ.month != seed.month
189
+
190
+ dates.push succ
191
+ end
192
+ dates.sort!
193
+ dates
194
+ end
195
+
196
+ # Generate an array of dates on +mday+ (the day-of-month, 1-31). For months
197
+ # in which the +mday+ is not present, no date will be generated.
198
+ #
199
+ # The period is a year, unless +month+ is non-nil, in which case it is just
200
+ # that month.
201
+ #
202
+ # Compare to Date.new(), which allows a single Date to be created with
203
+ # similar criteria.
204
+ def DateGen.bymonthday(year, month, mday)
205
+ months = month ? [ month ] : 1..12
206
+ dates = [ ]
207
+
208
+ months.each do |m|
209
+ begin
210
+ dates << Date.new(year, m, mday)
211
+ rescue ArgumentError
212
+ # Don't generate dates for invalid combinations (Feb 29, when it's not
213
+ # a leap year, for example).
214
+ #
215
+ # TODO - should we raise when month is out of range, or mday can never
216
+ # be in range (32)?
217
+ end
218
+ end
219
+ dates
220
+ end
221
+ end
222
+