vpim 0.16 → 0.17
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.
- data/lib/vpim/agent/plist.rb +86 -0
- data/lib/vpim/date.rb +1 -3
- data/lib/vpim/date.rb~ +198 -0
- data/lib/vpim/dirinfo.rb +47 -15
- data/lib/vpim/dirinfo.rb~ +242 -0
- data/lib/vpim/duration.rb +1 -3
- data/lib/vpim/duration.rb~ +121 -0
- data/lib/vpim/enumerator.rb +1 -3
- data/lib/vpim/enumerator.rb~ +29 -0
- data/lib/vpim/field.rb +141 -56
- data/lib/vpim/field.rb~ +594 -0
- data/lib/vpim/icalendar.rb +10 -16
- data/lib/vpim/icalendar.rb~ +548 -0
- data/lib/vpim/maker/vcard.rb +124 -46
- data/lib/vpim/maker/vcard.rb~ +382 -0
- data/lib/vpim/rfc2425.rb +30 -17
- data/lib/vpim/rfc2425.rb~ +246 -0
- data/lib/vpim/rrule.rb +2 -4
- data/lib/vpim/rrule.rb~ +482 -0
- data/lib/vpim/time.rb +1 -3
- data/lib/vpim/time.rb~ +42 -0
- data/lib/vpim/vcard.rb +84 -18
- data/lib/vpim/vcard.rb~ +232 -0
- data/lib/vpim/vevent.rb +1 -3
- data/lib/vpim/vevent.rb~ +381 -0
- data/lib/vpim/vpim.rb +61 -29
- data/lib/vpim/vpim.rb~ +61 -29
- metadata +16 -2
data/lib/vpim/time.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
|
3
|
-
|
4
|
-
Copyright (C) 2005 Sam Roberts
|
2
|
+
Copyright (C) 2006 Sam Roberts
|
5
3
|
|
6
4
|
This library is free software; you can redistribute it and/or modify it
|
7
5
|
under the same terms as the ruby language itself, see the file COPYING for
|
data/lib/vpim/time.rb~
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
=begin
|
2
|
+
$Id: time.rb,v 1.5 2005/02/02 02:55:59 sam Exp $
|
3
|
+
|
4
|
+
Copyright (C) 2005 Sam Roberts
|
5
|
+
|
6
|
+
This library is free software; you can redistribute it and/or modify it
|
7
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
8
|
+
details.
|
9
|
+
=end
|
10
|
+
|
11
|
+
require 'date'
|
12
|
+
|
13
|
+
# Extensions to builtin Time allowing addition to Time by multiples of other
|
14
|
+
# intervals than a second.
|
15
|
+
|
16
|
+
class Time
|
17
|
+
# Returns a new Time, +years+ later than this time. Feb 29 of a
|
18
|
+
# leap year will be rounded up to Mar 1 if the target date is not a leap
|
19
|
+
# year.
|
20
|
+
def plus_year(years)
|
21
|
+
Time.local(year + years, month, day, hour, min, sec, usec)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a new Time, +months+ later than this time. The day will be
|
25
|
+
# rounded down if it is not valid for that month.
|
26
|
+
# 31 plus 1 month will be on Feb 28!
|
27
|
+
def plus_month(months)
|
28
|
+
d = Date.new(year, month, day)
|
29
|
+
d >>= months
|
30
|
+
Time.local(d.year, d.month, d.day, hour, min, sec, usec)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns a new Time, +days+ later than this time.
|
34
|
+
# Does this do as I expect over DST? What if the hour doesn't exist
|
35
|
+
# in the next day, due to DST changes?
|
36
|
+
def plus_day(days)
|
37
|
+
d = Date.new(year, month, day)
|
38
|
+
d += days
|
39
|
+
Time.local(d.year, d.month, d.day, hour, min, sec, usec)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
data/lib/vpim/vcard.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
|
3
|
-
|
4
|
-
Copyright (C) 2005 Sam Roberts
|
2
|
+
Copyright (C) 2006 Sam Roberts
|
5
3
|
|
6
4
|
This library is free software; you can redistribute it and/or modify it
|
7
5
|
under the same terms as the ruby language itself, see the file COPYING for
|
@@ -30,7 +28,7 @@ module Vpim
|
|
30
28
|
#
|
31
29
|
# TODO - an open question is what exactly "vcard 2.1" support means. While I
|
32
30
|
# decode vCard 2.1 correctly, I don't encode it. Should I implement a
|
33
|
-
# transcoder,
|
31
|
+
# transcoder, so vCards can be decoded from either version, and then written
|
34
32
|
# to either version? Maybe an option to Field#encode()?
|
35
33
|
#
|
36
34
|
# TODO - there are very few methods that Vcard has that DirectoryInfo
|
@@ -39,17 +37,28 @@ module Vpim
|
|
39
37
|
# get the email addresses, or the name, or perhaps to set fields, like
|
40
38
|
# email=. What would be useful?
|
41
39
|
#
|
42
|
-
# =
|
40
|
+
# = Examples
|
41
|
+
#
|
42
|
+
# - link:ex_mkvcard.txt: example of creating a vCard
|
43
|
+
# - link:ex_cpvcard.txt: example of copying and them modifying a vCard
|
44
|
+
# - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
|
45
|
+
# - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards
|
46
|
+
# - link:ex_get_vcard_photo.txt: pull photo data from a vCard
|
47
|
+
# - link:ab-query.txt: query the OS X Address Book to find vCards
|
48
|
+
# - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful
|
49
|
+
# with Mutt (see link:README.mutt for details)
|
50
|
+
# - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a
|
51
|
+
# (small but) complete application contributed by Dane G. Avilla, thanks!
|
52
|
+
# - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards
|
53
|
+
# - link:vcf-dump.txt: utility for dumping contents of .vcf files
|
43
54
|
#
|
44
55
|
# Here's an example of encoding a simple vCard using the low-level API:
|
45
56
|
#
|
46
57
|
# card = Vpim::Vcard.create
|
47
|
-
# card << Vpim::DirectoryInfo::Field.create('email', user.name@example.com, 'type' =>
|
48
|
-
# card << Vpim::DirectoryInfo::Field.create('url',
|
49
|
-
# card << Vpim::DirectoryInfo::Field.create('fn',
|
58
|
+
# card << Vpim::DirectoryInfo::Field.create('email', 'user.name@example.com', 'type' => 'internet' )
|
59
|
+
# card << Vpim::DirectoryInfo::Field.create('url', 'http://www.example.com/user' )
|
60
|
+
# card << Vpim::DirectoryInfo::Field.create('fn', 'User Name' )
|
50
61
|
# puts card.to_s
|
51
|
-
#
|
52
|
-
# New! Use the Vpim::Maker::Vcard to make vCards!
|
53
62
|
class Vcard < DirectoryInfo
|
54
63
|
|
55
64
|
private_class_method :new
|
@@ -150,27 +159,84 @@ module Vpim
|
|
150
159
|
def [](name, type=nil)
|
151
160
|
fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
|
152
161
|
|
162
|
+
valued = fields.select { |f| f.value != '' }
|
163
|
+
if valued.first
|
164
|
+
fields = valued
|
165
|
+
end
|
166
|
+
|
153
167
|
# limit to preferred, if possible
|
154
|
-
pref = fields.
|
168
|
+
pref = fields.select { |f| f.pref? }
|
155
169
|
|
156
|
-
if
|
170
|
+
if pref.first
|
157
171
|
fields = pref
|
158
172
|
end
|
159
173
|
|
160
174
|
fields.first ? fields.first.value : nil
|
161
175
|
end
|
162
176
|
|
163
|
-
# The
|
164
|
-
|
177
|
+
# The name from a vCard, including all the components of the N: and FN:
|
178
|
+
# fields.
|
179
|
+
|
180
|
+
class Name
|
181
|
+
# family name from N:
|
182
|
+
attr_reader :family
|
183
|
+
# given name from N:
|
184
|
+
attr_reader :given
|
185
|
+
# additional names from N:
|
186
|
+
attr_reader :additional
|
187
|
+
# such as "Ms." or "Dr.", from N:
|
188
|
+
attr_reader :prefix
|
189
|
+
# such as "BFA", from N:
|
190
|
+
attr_reader :suffix
|
191
|
+
# all the components of N: formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}"
|
192
|
+
attr_reader :formatted
|
193
|
+
# full name, the FN: field, a formatted version of the N: field, probably
|
194
|
+
# in a form more align with the cultural conventions of the vCard owner
|
195
|
+
# than +formatted+ is
|
196
|
+
attr_reader :fullname
|
197
|
+
|
198
|
+
def initialize(n, fn) #:nodoc:
|
199
|
+
n = Vpim.decode_list(n, ';') do |item|
|
200
|
+
item.strip
|
201
|
+
end
|
202
|
+
|
203
|
+
@family = n[0] || ""
|
204
|
+
@given = n[1] || ""
|
205
|
+
@additional = n[2] || ""
|
206
|
+
@prefix = n[3] || ""
|
207
|
+
@suffix = n[4] || ""
|
208
|
+
@formatted = [ @prefix, @given, @additional, @family ].map{|i| i == '' ? nil : i}.compact.join(' ')
|
209
|
+
if @suffix != ''
|
210
|
+
@formatted << ', ' << @suffix
|
211
|
+
end
|
212
|
+
|
213
|
+
# FIXME - make calls to #fullname fail if fn is nil
|
214
|
+
@fullname = fn
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns the +name+ fields, N: and FN:, as a Name.
|
220
|
+
def name
|
221
|
+
unless instance_variables.include? '@name'
|
222
|
+
@name = Name.new(self['N'], self['FN'])
|
223
|
+
end
|
224
|
+
@name
|
225
|
+
end
|
226
|
+
|
227
|
+
# Deprecated.
|
228
|
+
def nickname #:nodoc:
|
165
229
|
nn = self['nickname']
|
166
|
-
if nn
|
167
|
-
nn =
|
168
|
-
nn = nn.sub(/\s+$/, '')
|
169
|
-
nn = nil if nn == ''
|
230
|
+
if nn && nn == ''
|
231
|
+
nn = nil
|
170
232
|
end
|
171
233
|
nn
|
172
234
|
end
|
173
235
|
|
236
|
+
# All the nicknames, [] if there are none.
|
237
|
+
def nicknames
|
238
|
+
enum_by_name('NICKNAME').select{|f| f.value != ''}.collect{|f| f.value}
|
239
|
+
end
|
174
240
|
|
175
241
|
# Returns the birthday as a Date on success, nil if there was no birthday
|
176
242
|
# field, and raises an error if the birthday field could not be expressed
|
data/lib/vpim/vcard.rb~
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
=begin
|
2
|
+
$Id: vcard.rb,v 1.13 2004/12/05 03:16:33 sam Exp $
|
3
|
+
|
4
|
+
Copyright (C) 2005 Sam Roberts
|
5
|
+
|
6
|
+
This library is free software; you can redistribute it and/or modify it
|
7
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
8
|
+
details.
|
9
|
+
=end
|
10
|
+
|
11
|
+
require 'vpim/dirinfo'
|
12
|
+
require 'vpim/vpim'
|
13
|
+
|
14
|
+
module Vpim
|
15
|
+
# A vCard, a specialization of a directory info object.
|
16
|
+
#
|
17
|
+
# The vCard format is specified by:
|
18
|
+
# - RFC2426: vCard MIME Directory Profile (vCard 3.0)
|
19
|
+
# - RFC2425: A MIME Content-Type for Directory Information
|
20
|
+
#
|
21
|
+
# This implements vCard 3.0, but it is also capable of decoding vCard 2.1.
|
22
|
+
#
|
23
|
+
# For information about:
|
24
|
+
# - link:rfc2426.txt: vCard MIME Directory Profile (vCard 3.0)
|
25
|
+
# - link:rfc2425.txt: A MIME Content-Type for Directory Information
|
26
|
+
# - http://www.imc.org/pdi/pdiproddev.html: vCard 2.1 Specifications
|
27
|
+
#
|
28
|
+
# vCards are usually transmitted in files with <code>.vcf</code>
|
29
|
+
# extensions.
|
30
|
+
#
|
31
|
+
# TODO - an open question is what exactly "vcard 2.1" support means. While I
|
32
|
+
# decode vCard 2.1 correctly, I don't encode it. Should I implement a
|
33
|
+
# transcoder, so vCards can be decoded from either version, and then written
|
34
|
+
# to either version? Maybe an option to Field#encode()?
|
35
|
+
#
|
36
|
+
# TODO - there are very few methods that Vcard has that DirectoryInfo
|
37
|
+
# doesn't. I could probably just do away with it entirely, but I suspect
|
38
|
+
# that there are methods that could be usefully added to Vcard, perhaps to
|
39
|
+
# get the email addresses, or the name, or perhaps to set fields, like
|
40
|
+
# email=. What would be useful?
|
41
|
+
#
|
42
|
+
# = Example
|
43
|
+
#
|
44
|
+
# Here's an example of encoding a simple vCard using the low-level API:
|
45
|
+
#
|
46
|
+
# card = Vpim::Vcard.create
|
47
|
+
# card << Vpim::DirectoryInfo::Field.create('email', 'user.name@example.com', 'type' => 'internet' )
|
48
|
+
# card << Vpim::DirectoryInfo::Field.create('url', 'http://www.example.com/user' )
|
49
|
+
# card << Vpim::DirectoryInfo::Field.create('fn', 'User Name' )
|
50
|
+
# puts card.to_s
|
51
|
+
#
|
52
|
+
# New! Use the Vpim::Maker::Vcard to make vCards!
|
53
|
+
class Vcard < DirectoryInfo
|
54
|
+
|
55
|
+
private_class_method :new
|
56
|
+
|
57
|
+
# Create a vCard 3.0 object with the minimum required fields, plus any
|
58
|
+
# +fields+ you want in the card (they can also be added later).
|
59
|
+
def Vcard.create(fields = [] )
|
60
|
+
fields.unshift Field.create('VERSION', "3.0")
|
61
|
+
super(fields, 'VCARD')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Decode a collection of vCards into an array of Vcard objects.
|
65
|
+
#
|
66
|
+
# +card+ can be either a String or an IO object.
|
67
|
+
#
|
68
|
+
# Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard),
|
69
|
+
# multiple vCards can be concatenated into a single directory info object.
|
70
|
+
# They may or may not be related. For example, AddressBook.app (the OS X
|
71
|
+
# contact manager) will export multiple selected cards in this format.
|
72
|
+
#
|
73
|
+
# Input data will be converted from unicode if it is detected. The heuristic
|
74
|
+
# is based on the first bytes in the string:
|
75
|
+
# - 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped
|
76
|
+
# - 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string
|
77
|
+
# is converted to UTF-8
|
78
|
+
# - 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string
|
79
|
+
# is converted to UTF-8
|
80
|
+
# - 0x00 'B' or 0x00 'b': UTF-16 (big-endian), the string is converted to UTF-8
|
81
|
+
# - 'B' 0x00 or 'b' 0x00: UTF-16 (little-endian), the string is converted to UTF-8
|
82
|
+
#
|
83
|
+
# If you know that you have only one vCard, then you can decode that
|
84
|
+
# single vCard by doing something like:
|
85
|
+
#
|
86
|
+
# vcard = Vcard.decode(card_data).first
|
87
|
+
#
|
88
|
+
# Note: Should the import encoding be remembered, so that it can be reencoded in
|
89
|
+
# the same format?
|
90
|
+
def Vcard.decode(card)
|
91
|
+
if card.respond_to? :to_str
|
92
|
+
string = card.to_str
|
93
|
+
elsif card.kind_of? IO
|
94
|
+
string = card.read(nil)
|
95
|
+
else
|
96
|
+
raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
|
97
|
+
end
|
98
|
+
|
99
|
+
case string
|
100
|
+
when /^\xEF\xBB\xBF/
|
101
|
+
string = string.sub("\xEF\xBB\xBF", '')
|
102
|
+
when /^\xFE\xFF/
|
103
|
+
arr = string.unpack('n*')
|
104
|
+
arr.shift
|
105
|
+
string = arr.pack('U*')
|
106
|
+
when /^\xFF\xFE/
|
107
|
+
arr = string.unpack('v*')
|
108
|
+
arr.shift
|
109
|
+
string = arr.pack('U*')
|
110
|
+
when /^\x00\x62/i
|
111
|
+
string = string.unpack('n*').pack('U*')
|
112
|
+
when /^\x62\x00/i
|
113
|
+
string = string.unpack('v*').pack('U*')
|
114
|
+
end
|
115
|
+
|
116
|
+
entities = Vpim.expand(Vpim.decode(string))
|
117
|
+
|
118
|
+
# Since all vCards must have a begin/end, the top-level should consist
|
119
|
+
# entirely of entities/arrays, even if its a single vCard.
|
120
|
+
if entities.detect { |e| ! e.kind_of? Array }
|
121
|
+
raise "Not a valid vCard"
|
122
|
+
end
|
123
|
+
|
124
|
+
vcards = []
|
125
|
+
|
126
|
+
for e in entities
|
127
|
+
vcards.push(new(e.flatten, 'VCARD'))
|
128
|
+
end
|
129
|
+
|
130
|
+
vcards
|
131
|
+
end
|
132
|
+
|
133
|
+
# The vCard version multiplied by 10 as an Integer. If no VERSION field
|
134
|
+
# is present (which is non-conformant), nil is returned. For example, a
|
135
|
+
# version 2.1 vCard would have a version of 21, and a version 3.0 vCard
|
136
|
+
# would have a version of 30.
|
137
|
+
def version
|
138
|
+
v = self["version"]
|
139
|
+
unless v
|
140
|
+
raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
|
141
|
+
end
|
142
|
+
v = v.to_f * 10
|
143
|
+
v = v.to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
# The value of the field named +name+, optionally limited to fields of
|
147
|
+
# type +type+. If no match is found, nil is returned, if multiple matches
|
148
|
+
# are found, the first match to have one of its type values be 'pref'
|
149
|
+
# (preferred) is returned, otherwise the first match is returned.
|
150
|
+
def [](name, type=nil)
|
151
|
+
fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
|
152
|
+
|
153
|
+
valued = fields.select { |f| f.value != '' }
|
154
|
+
if valued.first
|
155
|
+
fields = valued
|
156
|
+
end
|
157
|
+
|
158
|
+
# limit to preferred, if possible
|
159
|
+
pref = fields.select { |f| f.pref? }
|
160
|
+
|
161
|
+
if pref.first
|
162
|
+
fields = pref
|
163
|
+
end
|
164
|
+
|
165
|
+
fields.first ? fields.first.value : nil
|
166
|
+
end
|
167
|
+
|
168
|
+
=begin
|
169
|
+
TODO
|
170
|
+
def name
|
171
|
+
h = {} Create a Struct from the FN fields (maybe add the N field value, to?)
|
172
|
+
|
173
|
+
def h.to_s
|
174
|
+
concatenate the name fields nicely to make the name
|
175
|
+
end
|
176
|
+
end
|
177
|
+
=end
|
178
|
+
|
179
|
+
# The nickname or nil if there is none.
|
180
|
+
def nickname
|
181
|
+
nn = self['nickname']
|
182
|
+
if nn && nn == ''
|
183
|
+
nn = nil
|
184
|
+
end
|
185
|
+
nn
|
186
|
+
end
|
187
|
+
|
188
|
+
# All the nicknames, [] if there are none.
|
189
|
+
def nicknames
|
190
|
+
nn = self['nickname']
|
191
|
+
if nn && nn == ''
|
192
|
+
nn = nil
|
193
|
+
end
|
194
|
+
nn
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns the birthday as a Date on success, nil if there was no birthday
|
198
|
+
# field, and raises an error if the birthday field could not be expressed
|
199
|
+
# as a recurring event.
|
200
|
+
#
|
201
|
+
# Also, I have a lot of vCards with dates that look like:
|
202
|
+
# 678-09-23
|
203
|
+
# The 678 is garbage, but 09-23 is really the birthday. This substitutes the
|
204
|
+
# current year for the 3 digit year, and then converts to a Date.
|
205
|
+
def birthday
|
206
|
+
bday = self.field('BDAY')
|
207
|
+
|
208
|
+
return nil unless bday
|
209
|
+
|
210
|
+
begin
|
211
|
+
date = bday.to_date.first
|
212
|
+
|
213
|
+
rescue Vpim::InvalidEncodingError
|
214
|
+
if bday.value =~ /(\d+)-(\d+)-(\d+)/
|
215
|
+
y = $1.to_i
|
216
|
+
m = $2.to_i
|
217
|
+
d = $3.to_i
|
218
|
+
if(y < 1900)
|
219
|
+
y = Time.now.year
|
220
|
+
end
|
221
|
+
date = Date.new(y, m, d)
|
222
|
+
else
|
223
|
+
raise
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
date
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
data/lib/vpim/vevent.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
|
3
|
-
|
4
|
-
Copyright (C) 2005 Sam Roberts
|
2
|
+
Copyright (C) 2006 Sam Roberts
|
5
3
|
|
6
4
|
This library is free software; you can redistribute it and/or modify it
|
7
5
|
under the same terms as the ruby language itself, see the file COPYING for
|
data/lib/vpim/vevent.rb~
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
=begin
|
2
|
+
$Id: vevent.rb,v 1.12 2005/01/21 04:09:55 sam Exp $
|
3
|
+
|
4
|
+
Copyright (C) 2005 Sam Roberts
|
5
|
+
|
6
|
+
This library is free software; you can redistribute it and/or modify it
|
7
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
8
|
+
details.
|
9
|
+
=end
|
10
|
+
|
11
|
+
require 'vpim/dirinfo'
|
12
|
+
require 'vpim/field'
|
13
|
+
require 'vpim/rfc2425'
|
14
|
+
require 'vpim/vpim'
|
15
|
+
|
16
|
+
=begin
|
17
|
+
A vTodo that is done:
|
18
|
+
|
19
|
+
BEGIN:VTODO
|
20
|
+
COMPLETED:20040303T050000Z
|
21
|
+
DTSTAMP:20040304T011707Z
|
22
|
+
DTSTART;TZID=Canada/Eastern:20030524T115238
|
23
|
+
SEQUENCE:2
|
24
|
+
STATUS:COMPLETED
|
25
|
+
SUMMARY:Wash Car
|
26
|
+
UID:E7609713-8E13-11D7-8ACC-000393AD088C
|
27
|
+
END:VTODO
|
28
|
+
|
29
|
+
BEGIN:VTODO
|
30
|
+
DTSTAMP:20030909T015533Z
|
31
|
+
DTSTART;TZID=Canada/Eastern:20030808T000000
|
32
|
+
SEQUENCE:1
|
33
|
+
SUMMARY:Renew Passport
|
34
|
+
UID:EC76B256-BBE9-11D7-8401-000393AD088C
|
35
|
+
END:VTODO
|
36
|
+
|
37
|
+
|
38
|
+
=end
|
39
|
+
|
40
|
+
module Vpim
|
41
|
+
class Icalendar
|
42
|
+
class Vtodo
|
43
|
+
def initialize(fields) #:nodoc:
|
44
|
+
@fields = fields
|
45
|
+
|
46
|
+
outer, inner = Vpim.outer_inner(fields)
|
47
|
+
|
48
|
+
@properties = Vpim::DirectoryInfo.create(outer)
|
49
|
+
|
50
|
+
@elements = inner
|
51
|
+
|
52
|
+
# TODO - don't get properties here, put the accessor in a module,
|
53
|
+
# which can cache the results.
|
54
|
+
|
55
|
+
@summary = @properties.text('SUMMARY').first
|
56
|
+
@description = @properties.text('DESCRIPTION').first
|
57
|
+
@comment = @properties.text('COMMENT').first
|
58
|
+
@location = @properties.text('LOCATION').first
|
59
|
+
@status = @properties.text('STATUS').first
|
60
|
+
@uid = @properties.text('UID').first
|
61
|
+
@priority = @properties.text('PRIORITY').first
|
62
|
+
|
63
|
+
# See "TODO - fields" in dirinfo.rb
|
64
|
+
@dtstamp = @properties.field('dtstamp')
|
65
|
+
@dtstart = @properties.field('dtstart')
|
66
|
+
@dtend = @properties.field('dtend')
|
67
|
+
@duration = @properties.field('duration')
|
68
|
+
@due = @properties.field('due')
|
69
|
+
@rrule = @properties['rrule']
|
70
|
+
|
71
|
+
# Need to seperate status-handling out into a module...
|
72
|
+
@status_values = [ 'COMPLETED' ];
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :description, :summary, :comment, :location
|
77
|
+
attr_reader :properties, :fields # :nodoc:
|
78
|
+
|
79
|
+
# Create a new Vtodo object.
|
80
|
+
#
|
81
|
+
# If specified, +fields+ must be either an array of Field objects to
|
82
|
+
# add, or a Hash of String names to values that will be used to build
|
83
|
+
# Field objects. The latter is a convenient short-cut allowing the Field
|
84
|
+
# objects to be created for you when called like:
|
85
|
+
#
|
86
|
+
# Vtodo.create('SUMMARY' => "buy mangos")
|
87
|
+
#
|
88
|
+
# TODO - maybe todos are usually created in a particular way? I can
|
89
|
+
# make it easier. Ideally, I would like to make it hard to encode an invalid
|
90
|
+
# Event.
|
91
|
+
def Vtodo.create(fields=[])
|
92
|
+
di = DirectoryInfo.create([], 'VTODO')
|
93
|
+
|
94
|
+
Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
|
95
|
+
|
96
|
+
new(di.to_a)
|
97
|
+
end
|
98
|
+
|
99
|
+
=begin
|
100
|
+
I think that the initialization shouldn't be done in the #initialize, so, for example,
|
101
|
+
@status = @properties.text('STATUS').first
|
102
|
+
should be in the method below.
|
103
|
+
|
104
|
+
That way, I can construct a Vtodo by just including a module for each field that is allowed
|
105
|
+
in a Vtodo, simply.
|
106
|
+
=end
|
107
|
+
def status
|
108
|
+
if(!@status); return nil; end
|
109
|
+
|
110
|
+
s = @status.upcase
|
111
|
+
|
112
|
+
unless @status_values.include?(s)
|
113
|
+
raise Vpim::InvalidEncodingError, "Invalid status '#{@status}'"
|
114
|
+
end
|
115
|
+
|
116
|
+
s
|
117
|
+
end
|
118
|
+
|
119
|
+
# +priority+ is a number from 1 to 9, with 1 being the highest and 0
|
120
|
+
# meaning "no priority", equivalent to not specifying the PRIORITY field.
|
121
|
+
# Other values are reserved by RFC2446.
|
122
|
+
def priority
|
123
|
+
p = @priority ? @priority.to_i : 0
|
124
|
+
|
125
|
+
if( p < 0 || p > 9 )
|
126
|
+
raise Vpim::InvalidEncodingError, 'Invalid priority #{@priority} - it must be 0-9!'
|
127
|
+
end
|
128
|
+
p
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
module Vpim
|
135
|
+
class Icalendar
|
136
|
+
class Vevent
|
137
|
+
def initialize(fields) #:nodoc:
|
138
|
+
@fields = fields
|
139
|
+
|
140
|
+
outer, inner = Vpim.outer_inner(fields)
|
141
|
+
|
142
|
+
@properties = Vpim::DirectoryInfo.create(outer)
|
143
|
+
|
144
|
+
@elements = inner
|
145
|
+
|
146
|
+
# TODO - don't get properties here, put the accessor in a module,
|
147
|
+
# which can cache the results.
|
148
|
+
|
149
|
+
@summary = @properties.text('SUMMARY').first
|
150
|
+
@description = @properties.text('DESCRIPTION').first
|
151
|
+
@comment = @properties.text('COMMENT').first
|
152
|
+
@location = @properties.text('LOCATION').first
|
153
|
+
@status = @properties.text('STATUS').first
|
154
|
+
@uid = @properties.text('UID').first
|
155
|
+
|
156
|
+
# See "TODO - fields" in dirinfo.rb
|
157
|
+
@dtstamp = @properties.field('dtstamp')
|
158
|
+
@dtstart = @properties.field('dtstart')
|
159
|
+
@dtend = @properties.field('dtend')
|
160
|
+
@duration = @properties.field('duration')
|
161
|
+
@rrule = @properties['rrule']
|
162
|
+
|
163
|
+
# Need to seperate status-handling out into a module...
|
164
|
+
@status_values = [ 'TENTATIVE', 'CONFIRMED', 'CANCELLED' ];
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
# Create a new Vevent object. All events must have a DTSTART field,
|
169
|
+
# specify it as either a Time or a Date in +start+, it defaults to "now"
|
170
|
+
# (is this useful?).
|
171
|
+
#
|
172
|
+
# If specified, +fields+ must be either an array of Field objects to
|
173
|
+
# add, or a Hash of String names to values that will be used to build
|
174
|
+
# Field objects. The latter is a convenient short-cut allowing the Field
|
175
|
+
# objects to be created for you when called like:
|
176
|
+
#
|
177
|
+
# Vevent.create(Date.today, 'SUMMARY' => "today's event")
|
178
|
+
#
|
179
|
+
# TODO - maybe events are usually created in a particular way? With a
|
180
|
+
# start/duration or a start/end? Maybe I can make it easier. Ideally, I
|
181
|
+
# would like to make it hard to encode an invalid Event.
|
182
|
+
def Vevent.create(start = Time.now, fields=[])
|
183
|
+
dtstart = DirectoryInfo::Field.create('DTSTART', start)
|
184
|
+
di = DirectoryInfo.create([ dtstart ], 'VEVENT')
|
185
|
+
|
186
|
+
Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
|
187
|
+
|
188
|
+
new(di.to_a)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Creates a yearly repeating event, such as for a birthday.
|
192
|
+
def Vevent.create_yearly(date, summary)
|
193
|
+
create(
|
194
|
+
date,
|
195
|
+
'SUMMARY' => summary.to_str,
|
196
|
+
'RRULE' => 'FREQ=YEARLY'
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
attr_reader :description, :summary, :comment, :location
|
201
|
+
attr_reader :properties, :fields # :nodoc:
|
202
|
+
|
203
|
+
#--
|
204
|
+
# The methods below should be shared, somehow, by all calendar components, not just Events.
|
205
|
+
#++
|
206
|
+
|
207
|
+
# Accept an event invitation. The +invitee+ is the Address that wishes
|
208
|
+
# to accept the event invitation as confirmed.
|
209
|
+
def accept(invitee)
|
210
|
+
# The event created is identical to this one, but
|
211
|
+
# - without the attendees
|
212
|
+
# - with the invitee added with a PARTSTAT of ACCEPTED
|
213
|
+
invitee = invitee.copy
|
214
|
+
invitee.partstat = 'ACCEPTED'
|
215
|
+
|
216
|
+
fields = []
|
217
|
+
|
218
|
+
@properties.each_with_index do
|
219
|
+
|f,i|
|
220
|
+
|
221
|
+
# put invitee in as field[1]
|
222
|
+
fields << invitee.field if i == 1
|
223
|
+
|
224
|
+
fields << f unless f.name? 'ATTENDEE'
|
225
|
+
end
|
226
|
+
|
227
|
+
Vevent.new(fields)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Status values are not rejected during decoding. However, if the
|
231
|
+
# status is requested, and it's value is not one of the defined
|
232
|
+
# allowable values, an exception is raised.
|
233
|
+
def status
|
234
|
+
if(!@status); return nil; end
|
235
|
+
|
236
|
+
s = @status.upcase
|
237
|
+
|
238
|
+
unless @status_values.include?(s)
|
239
|
+
raise Vpim::InvalidEncodingError, "Invalid status '#{@status}'"
|
240
|
+
end
|
241
|
+
|
242
|
+
s
|
243
|
+
end
|
244
|
+
|
245
|
+
# TODO - def status? ...
|
246
|
+
|
247
|
+
# TODO - def status= ...
|
248
|
+
|
249
|
+
# The unique identifier of this calendar component, a string. It cannot be
|
250
|
+
# nil, if it is not found in the component, the calendar is malformed, and
|
251
|
+
# this method will raise an exception.
|
252
|
+
def uid
|
253
|
+
if(!@uid)
|
254
|
+
raise Vpim::InvalidEncodingError, 'Invalid component - no UID field was found!'
|
255
|
+
end
|
256
|
+
|
257
|
+
@uid
|
258
|
+
end
|
259
|
+
|
260
|
+
# The time stamp for this calendar component. Describe what this is....
|
261
|
+
# This field is required!
|
262
|
+
def dtstamp
|
263
|
+
if(!@dtstamp)
|
264
|
+
raise Vpim::InvalidEncodingError, 'Invalid component - no DTSTAMP field was found!'
|
265
|
+
end
|
266
|
+
|
267
|
+
@dtstamp.to_time.first
|
268
|
+
end
|
269
|
+
|
270
|
+
# The start time for this calendar component. Describe what this is....
|
271
|
+
# This field is required!
|
272
|
+
def dtstart
|
273
|
+
if(!@dtstart)
|
274
|
+
raise Vpim::InvalidEncodingError, 'Invalid component - no DTSTART field was found!'
|
275
|
+
end
|
276
|
+
|
277
|
+
@dtstart.to_time.first
|
278
|
+
end
|
279
|
+
|
280
|
+
=begin
|
281
|
+
# Set the start time for the event to +start+, a Time object.
|
282
|
+
# TODO - def dtstart=(start) ... start should be allowed to be Time/Date/DateTime
|
283
|
+
=end
|
284
|
+
|
285
|
+
# The duration in seconds of a Event, Todo, or Vfreebusy component, or
|
286
|
+
# for Alarms, the delay period prior to repeating the alarm. The
|
287
|
+
# duration is calculated from the DTEND and DTBEGIN fields if the
|
288
|
+
# DURATION field is not present. Durations of zero seconds are possible.
|
289
|
+
def duration
|
290
|
+
if(!@duration)
|
291
|
+
return nil unless @dtend
|
292
|
+
|
293
|
+
b = dtstart
|
294
|
+
e = dtend
|
295
|
+
|
296
|
+
return (e - b).to_i
|
297
|
+
end
|
298
|
+
|
299
|
+
Icalendar.decode_duration(@duration.value_raw)
|
300
|
+
end
|
301
|
+
|
302
|
+
# The end time for this calendar component. For an Event, if there is no
|
303
|
+
# end time, then nil is returned, and the event takes up no time.
|
304
|
+
# However, the end time will be calculated from the event duration, if
|
305
|
+
# present.
|
306
|
+
def dtend
|
307
|
+
if(@dtend)
|
308
|
+
@dtend.to_time.first
|
309
|
+
elsif duration
|
310
|
+
dtstart + duration
|
311
|
+
else
|
312
|
+
nil
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# The recurrence rule, if any, for this event. Recurrence starts at the
|
317
|
+
# DTSTART time.
|
318
|
+
def rrule
|
319
|
+
@rrule
|
320
|
+
end
|
321
|
+
|
322
|
+
# The times this event occurs, as a Vpim::Rrule.
|
323
|
+
#
|
324
|
+
# Note: the event may occur only once.
|
325
|
+
#
|
326
|
+
# Note: occurences are currently calculated only from DTSTART and RRULE,
|
327
|
+
# no allowance for EXDATE or other fields is made.
|
328
|
+
def occurences
|
329
|
+
Vpim::Rrule.new(dtstart, @rrule)
|
330
|
+
end
|
331
|
+
|
332
|
+
# Check if this event overlaps with the time period later than or equal to +t0+, but
|
333
|
+
# earlier than +t1+.
|
334
|
+
def occurs_in?(t0, t1)
|
335
|
+
occurences.each_until(t1).detect { |t| tend = t + (duration || 0); tend > t0 }
|
336
|
+
end
|
337
|
+
|
338
|
+
# Return the event organizer, an object of Icalendar::Address (or nil if
|
339
|
+
# there is no ORGANIZER field).
|
340
|
+
#
|
341
|
+
# TODO - verify that it is illegal for there to be more than one
|
342
|
+
# ORGANIZER, if more than one is allowed, this needs to return an array.
|
343
|
+
def organizer
|
344
|
+
unless instance_variables.include? "@organizer"
|
345
|
+
@organizer = @properties.field('ORGANIZER')
|
346
|
+
|
347
|
+
if @organizer
|
348
|
+
@organizer = Icalendar::Address.new(@organizer)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
@organizer.freeze
|
352
|
+
end
|
353
|
+
|
354
|
+
# Return an array of attendees, an empty array if there are none. The
|
355
|
+
# attendees are objects of Icalendar::Address. If +uri+ is specified
|
356
|
+
# only the return the attendees with this +uri+.
|
357
|
+
def attendees(uri = nil)
|
358
|
+
unless instance_variables.include? "@attendees"
|
359
|
+
@attendees = @properties.enum_by_name('ATTENDEE').map { |a| Icalendar::Address.new(a).freeze }
|
360
|
+
@attendees.freeze
|
361
|
+
end
|
362
|
+
if uri
|
363
|
+
@attendees.select { |a| a == uri } .freeze
|
364
|
+
else
|
365
|
+
@attendees
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Return true if the +uri+, usually a mailto: URI, is an attendee.
|
370
|
+
def attendee?(uri)
|
371
|
+
attendees.include? uri
|
372
|
+
end
|
373
|
+
|
374
|
+
# CONTACT - value is text, parameters are ALTREP and LANGUAGE.
|
375
|
+
#
|
376
|
+
# textual contact information, or an altrep referring to a URI pointing
|
377
|
+
# at a vCard or LDAP entry...
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|