vpim 0.16

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.
@@ -0,0 +1,337 @@
1
+ =begin
2
+ $Id: vcard.rb,v 1.7 2005/02/04 21:09:34 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/vcard'
12
+
13
+ module Vpim
14
+ module Maker
15
+ # A helper class to assist in building a vCard.
16
+ #
17
+ # This idea is modelled after ruby 1.8's rss/maker classes. Perhaps all these methods
18
+ # should be added to Vpim::Vcard?
19
+ class Vcard
20
+ # Make a vCard for +full_name+.
21
+ #
22
+ # Yields +card+, a Vpim::Maker::Vcard to which fields can be added, and returns a Vpim::Vcard.
23
+ #
24
+ # Note that calling #add_name is required, all other fields are optional.
25
+ def Vcard.make(full_name, &block) # :yields: +card+
26
+ new(full_name).make(&block)
27
+ end
28
+
29
+ def make # :nodoc:
30
+ yield self
31
+ if !@initialized_N
32
+ raise Vpim::InvalidEncodingError, 'It is mandatory to have a N field, see #add_name.'
33
+ end
34
+ @card
35
+ end
36
+
37
+ private
38
+
39
+ def initialize(full_name) # :nodoc:
40
+ @card = Vpim::Vcard::create
41
+ @card << Vpim::DirectoryInfo::Field.create('FN', full_name )
42
+ @initialized_N = false
43
+ # pp @card
44
+ end
45
+
46
+ public
47
+
48
+ # Add an arbitrary Field, +field+.
49
+ def add_field(field)
50
+ @card << field
51
+ end
52
+
53
+ # Add a name field, N.
54
+ #
55
+ # Warning: This is the only mandatory field, besides the full name, which
56
+ # is added from Vcard.make's +full_name+.
57
+ #
58
+ # Attributes of N are:
59
+ # - family: family name
60
+ # - given: given name
61
+ # - additional: additional names
62
+ # - prefix: such as "Ms." or "Dr."
63
+ # - suffix: such as "BFA", or "Sensei"
64
+ #
65
+ # All attributes are optional.
66
+ #
67
+ # FIXME: is it possible to deduce given/family from the full_name?
68
+ #
69
+ # FIXME: Each attribute can currently only have a single String value.
70
+ #
71
+ # FIXME: Need to escape specials in the String.
72
+ def add_name # :yield: n
73
+ x = Struct.new(:family, :given, :additional, :prefix, :suffix).new
74
+ yield x
75
+ @card << Vpim::DirectoryInfo::Field.create(
76
+ 'N',
77
+ x.map { |s| s ? s.to_str : '' }
78
+ )
79
+ @initialized_N = true
80
+ self
81
+ end
82
+
83
+ # Add a address field, ADR.
84
+ #
85
+ # Attributes of ADR that describe the address are:
86
+ # - pobox: post office box
87
+ # - extended: seldom used, its not clear what it is for
88
+ # - street: street address, multiple components should be separated by a comma, ','
89
+ # - locality: usually the city
90
+ # - region: usually the province or state
91
+ # - postalcode: postal code
92
+ # - country: country name, no standard for country naming is specified
93
+ #
94
+ # Attributes that describe how the address is used, and customary values, are:
95
+ # - location: home, work - often used, can be set to other values
96
+ # - preferred: true - often used, set if this is the preferred address
97
+ # - delivery: postal, parcel, dom (domestic), intl (international) - rarely used
98
+ #
99
+ # All attributes are optional. #location and #home can be set to arrays of
100
+ # strings.
101
+ #
102
+ # TODO: Add #label to support LABEL.
103
+ #
104
+ # FIXME: Need to escape specials in the String.
105
+ def add_addr # :yield: adr
106
+ x = Struct.new(
107
+ :location, :preferred, :delivery,
108
+ :pobox, :extended, :street, :locality, :region, :postalcode, :country
109
+ ).new
110
+ yield x
111
+
112
+ values = x.to_a[3, 7].map { |s| s ? s.to_str : '' }
113
+
114
+ # All these attributes go into the TYPE parameter.
115
+ params = [ x[:location], x[:delivery] ]
116
+ params << 'pref' if x[:preferred]
117
+ params = params.flatten.uniq.compact.map { |s| s.to_str }
118
+
119
+ paramshash = {}
120
+
121
+ paramshash['type'] = params if params.first
122
+
123
+ @card << Vpim::DirectoryInfo::Field.create( 'ADR', values, paramshash)
124
+ self
125
+ end
126
+
127
+ # Add a telephone number field, TEL.
128
+ #
129
+ # +number+ is supposed to be a "X.500 Telephone Number" according to RFC 2426, if you happen
130
+ # to be familiar with that. Otherwise, anything that looks like a phone number should be OK.
131
+ #
132
+ # Attributes of TEL are:
133
+ # - location: home, work, msg, cell, car, pager - often used, can be set to other values
134
+ # - preferred: true - often used, set if this is the preferred telephone number
135
+ # - capability: voice,fax,video,bbs,modem,isdn,pcs - fax is useful, the others are rarely used
136
+ #
137
+ # All attributes are optional, and so is the block.
138
+ def add_tel(number) # :yield: tel
139
+ params = {}
140
+ if block_given?
141
+ x = Struct.new( :location, :preferred, :capability ).new
142
+
143
+ yield x
144
+
145
+ x[:preferred] = 'pref' if x[:preferred]
146
+
147
+ types = x.to_a.flatten.uniq.compact.map { |s| s.to_str }
148
+
149
+ params['type'] = types if types.first
150
+ end
151
+
152
+ @card << Vpim::DirectoryInfo::Field.create( 'TEL', number, params)
153
+ self
154
+ end
155
+
156
+ # Add a email address field, EMAIL.
157
+ #
158
+ # Attributes of EMAIL are:
159
+ # - location: home, work - often used, can be set to other values
160
+ # - preferred: true - often used, set if this is the preferred email address
161
+ # - protocol: internet,x400 - internet is the default, set this for other kinds
162
+ #
163
+ # All attributes are optional, and so is the block.
164
+ def add_email(email) # :yield: email
165
+ params = {}
166
+ if block_given?
167
+ x = Struct.new( :location, :preferred, :protocol ).new
168
+
169
+ yield x
170
+
171
+ x[:preferred] = 'pref' if x[:preferred]
172
+
173
+ types = x.to_a.flatten.uniq.compact.map { |s| s.to_str }
174
+
175
+ params['type'] = types if types.first
176
+ end
177
+
178
+ @card << Vpim::DirectoryInfo::Field.create( 'EMAIL', email, params)
179
+ self
180
+ end
181
+
182
+ # Add a nickname field, NICKNAME.
183
+ def nickname=(nickname)
184
+ @card << Vpim::DirectoryInfo::Field.create( 'NICKNAME', nickname );
185
+ end
186
+
187
+ # Add a birthday field, BDAY.
188
+ #
189
+ # +birthday+ must be a time or date object.
190
+ #
191
+ # Warning: It may confuse both humans and software if you add multiple
192
+ # birthdays.
193
+ def birthday=(birthday)
194
+ if !birthday.respond_to? :month
195
+ raise Vpim::InvalidEncodingError, 'birthday doesn\'t have #month, so it is not a date or time object.'
196
+ end
197
+ @card << Vpim::DirectoryInfo::Field.create( 'BDAY', birthday );
198
+ end
199
+ =begin
200
+ TODO - need text=() implemented in Field
201
+
202
+ # Add a note field, NOTE. It can contain newlines, they will be escaped.
203
+ def note=(note)
204
+ @card << Vpim::DirectoryInfo::Field.create( 'NOTE', note );
205
+ end
206
+ =end
207
+
208
+ # Add an instant-messaging/point of presence address field, IMPP. The address
209
+ # is a URL, with the syntax depending on the protocol.
210
+ #
211
+ # Attributes of IMPP are:
212
+ # - preferred: true - set if this is the preferred address
213
+ # - location: home, work, mobile - location of address
214
+ # - purpose: personal,business - purpose of communications
215
+ #
216
+ # All attributes are optional, and so is the block.
217
+ #
218
+ # The URL syntaxes for the messaging schemes is fairly complicated, so I
219
+ # don't try and build the URLs here, maybe in the future. This forces
220
+ # the user to know the URL for their own address, hopefully not too much
221
+ # of a burden.
222
+ #
223
+ # IMPP is defined in draft-jennings-impp-vcard-04.txt. It refers to the
224
+ # URI scheme of a number of messaging protocols, but doesn't give
225
+ # references to all of them:
226
+ # - "xmpp" indicates to use XMPP, draft-saintandre-xmpp-uri-06.txt
227
+ # - "irc" or "ircs" indicates to use IRC, draft-butcher-irc-url-04.txt
228
+ # - "sip" indicates to use SIP/SIMPLE, RFC 3261
229
+ # - "im" or "pres" indicates to use a CPIM or CPP gateway, RFC 3860 and RFC 3859
230
+ # - "ymsgr" indicates to use yahoo
231
+ # - "msn" might indicate to use Microsoft messenger
232
+ # - "aim" indicates to use AOL
233
+ #
234
+ def add_impp(url) # :yield: impp
235
+ params = {}
236
+
237
+ if block_given?
238
+ x = Struct.new( :location, :preferred, :purpose ).new
239
+
240
+ yield x
241
+
242
+ x[:preferred] = 'pref' if x[:preferred]
243
+
244
+ types = x.to_a.flatten.uniq.compact.map { |s| s.to_str }
245
+
246
+ params['type'] = types if types.first
247
+ end
248
+
249
+ @card << Vpim::DirectoryInfo::Field.create( 'IMPP', url, params)
250
+ self
251
+ end
252
+
253
+ # Add an Apple style AIM account name, +xaim+ is an AIM screen name.
254
+ #
255
+ # I don't know if this is conventional, or supported by anything other
256
+ # than AddressBook.app, but an example is:
257
+ # X-AIM;type=HOME;type=pref:exampleaccount
258
+ #
259
+ # Attributes of X-AIM are:
260
+ # - preferred: true - set if this is the preferred address
261
+ # - location: home, work, mobile - location of address
262
+ #
263
+ # All attributes are optional, and so is the block.
264
+ def add_x_aim(xaim) # :yield: xaim
265
+ params = {}
266
+
267
+ if block_given?
268
+ x = Struct.new( :location, :preferred ).new
269
+
270
+ yield x
271
+
272
+ x[:preferred] = 'pref' if x[:preferred]
273
+
274
+ types = x.to_a.flatten.uniq.compact.map { |s| s.to_str }
275
+
276
+ params['type'] = types if types.first
277
+ end
278
+
279
+ @card << Vpim::DirectoryInfo::Field.create( 'X-AIM', xaim, params)
280
+ self
281
+ end
282
+
283
+
284
+ # Add a photo field, PHOTO.
285
+ #
286
+ # Attributes of PHOTO are:
287
+ # - image: set to image data to inclue inline
288
+ # - link: set to the URL of the image data
289
+ # - type: string identifying the image type, supposed to be an "IANA registered image format",
290
+ # or a non-registered image format (usually these start with an x-)
291
+ #
292
+ # An error will be raised if neither image or link is set, or if both image
293
+ # and link is set.
294
+ #
295
+ # Setting type is optional for a link image, because either the URL, the
296
+ # image file extension, or a HTTP Content-Type may specify the type. If
297
+ # it's not a link, setting type is mandatory, though it can be set to an
298
+ # empty string, <code>''</code>, if the type is unknown.
299
+ #
300
+ # TODO - I'm not sure about this API. I'm thinking maybe it should be
301
+ # #add_photo(image, type), and that I should detect when the image is a
302
+ # URL, and make type mandatory if it wasn't a URL.
303
+ def add_photo # :yield: photo
304
+ x = Struct.new(:image, :link, :type).new
305
+ yield x
306
+ if x[:image] && x[:link]
307
+ raise Vpim::InvalidEncodingError, 'Image is not allowed to be both inline and a link.'
308
+ end
309
+
310
+ value = x[:image] || x[:link]
311
+
312
+ if !value
313
+ raise Vpim::InvalidEncodingError, 'A image link or inline data must be provided.'
314
+ end
315
+
316
+ params = {}
317
+
318
+ # Don't set type to the empty string.
319
+ params['type'] = x[:type] if( x[:type] && x[:type].length > 0 )
320
+
321
+ if x[:link]
322
+ params['value'] = 'uri'
323
+ else # it's inline, base-64 encode it
324
+ params['encoding'] = :b64
325
+ if !x[:type]
326
+ raise Vpim::InvalidEncodingError, 'Inline image data must have it\'s type set.'
327
+ end
328
+ end
329
+
330
+ @card << Vpim::DirectoryInfo::Field.create( 'PHOTO', value, params )
331
+ self
332
+ end
333
+
334
+ end
335
+ end
336
+ end
337
+
@@ -0,0 +1,247 @@
1
+ =begin
2
+ $Id: rfc2425.rb,v 1.10 2005/01/01 17:17:01 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/vpim'
12
+
13
+ module Vpim
14
+ # Contains regular expression strings for the EBNF of RFC 2425.
15
+ module Bnf #:nodoc:
16
+
17
+ # 1*(ALPHA / DIGIT / "-")
18
+ # Note: I think I can add A-Z here, and get rid of the "i" matches elsewhere.
19
+ # Note: added '_' to allowed because its produced by Notes - X-LOTUS-CHILD_UID
20
+ NAME = '[-a-z0-9_]+'
21
+
22
+ # <"> <Any character except CTLs, DQUOTE> <">
23
+ QSTR = '"([^"]*)"'
24
+
25
+ # *<Any character except CTLs, DQUOTE, ";", ":", ",">
26
+ PTEXT = '([^";:,]+)'
27
+
28
+ # param-value = ptext / quoted-string
29
+ PVALUE = "(?:#{QSTR}|#{PTEXT})"
30
+
31
+ # param = name "=" param-value *("," param-value)
32
+ # Note: v2.1 allows a type or encoding param-value to appear without the type=
33
+ # or the encoding=. This is hideous, but we try and support it, if there
34
+ # is no "=", then $2 will be "", and we will treat it as a v2.1 param.
35
+ PARAM = ";(#{NAME})(=?)((?:#{PVALUE})?(?:,#{PVALUE})*)"
36
+
37
+ # V3.0: contentline = [group "."] name *(";" param) ":" value
38
+ # V2.1: contentline = *( group "." ) name *(";" param) ":" value
39
+ #
40
+ # We accept the V2.1 syntax for backwards compatibility.
41
+ #LINE = "((?:#{NAME}\\.)*)?(#{NAME})([^:]*)\:(.*)"
42
+ LINE = "^((?:#{NAME}\\.)*)?(#{NAME})((?:#{PARAM})*):(.*)$"
43
+
44
+ # date = date-fullyear ["-"] date-month ["-"] date-mday
45
+ # date-fullyear = 4 DIGIT
46
+ # date-month = 2 DIGIT
47
+ # date-mday = 2 DIGIT
48
+ DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
49
+
50
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
51
+ # time-hour = 2 DIGIT
52
+ # time-minute = 2 DIGIT
53
+ # time-second = 2 DIGIT
54
+ # time-secfrac = "," 1*DIGIT
55
+ # time-zone = "Z" / time-numzone
56
+ # time-numzome = sign time-hour [":"] time-minute
57
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
58
+ end
59
+ end
60
+
61
+ module Vpim
62
+ # Split on \r\n or \n to get the lines, unfold continued lines (they
63
+ # start with ' ' or \t), and return the array of unfolded lines.
64
+ #
65
+ # This also implements the (invalid) encoding convention of allowing empty
66
+ # lines to be inserted for readability - it does this by dropping
67
+ # zero-length lines.
68
+ def Vpim.unfold(card) #:nodoc:
69
+ unfolded = []
70
+
71
+ card.split(/\r?\n/).each do
72
+ |line|
73
+
74
+ # If it's a continuation line, add it to the last.
75
+ # If it's an empty line, drop it from the input.
76
+ if( line =~ /^[ \t]/ )
77
+ unfolded << unfolded.pop + line[1, line.size-1]
78
+ elsif( line =~ /^$/ )
79
+ else
80
+ unfolded << line
81
+ end
82
+ end
83
+
84
+ unfolded
85
+ end
86
+
87
+ # Convert a +sep+-seperated list of values into an array of values.
88
+ def Vpim.decode_list(value, sep = ',') # :nodoc:
89
+ list = []
90
+
91
+ value.each(sep) {
92
+ |item|
93
+ list << yield(item) unless item =~ %r{^\s*#{sep}?$}
94
+ }
95
+ list
96
+ end
97
+
98
+ # Convert a RFC 2425 date into an array of [year, month, day].
99
+ def Vpim.decode_date(v) # :nodoc:
100
+ unless v =~ %r{\s*#{Bnf::DATE}\s*}
101
+ raise Vpim::InvalidEncodingError, "date not valid (#{v})"
102
+ end
103
+ [$1.to_i, $2.to_i, $3.to_i]
104
+ end
105
+
106
+ # Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445
107
+ # does not. I choose to encode to the subset that is valid for both.
108
+
109
+ # Encode a Date object as "yyyymmdd".
110
+ def Vpim.encode_date(d) # :nodoc:
111
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
112
+ end
113
+
114
+ # Encode a Date object as "yyyymmdd".
115
+ def Vpim.encode_time(d) # :nodoc:
116
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
117
+ end
118
+
119
+ # Encode a Time or DateTime object as "yyyymmddThhmmss"
120
+ def Vpim.encode_date_time(d) # :nodoc:
121
+ "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ]
122
+ end
123
+
124
+ # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
125
+ def Vpim.decode_time(v) # :nodoc:
126
+ unless match = %r{\s*#{Bnf::TIME}\s*}.match(v)
127
+ raise Vpim::InvalidEncodingError, "time not valid (#{v})"
128
+ end
129
+ hour, min, sec, secfrac, tz = match.to_a[1..5]
130
+
131
+ [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
132
+ end
133
+
134
+ # Convert a RFC 2425 date-time into an array of [hour,min,sec,secfrac,timezone]
135
+ def Vpim.decode_date_time(v) # :nodoc:
136
+ unless match = %r{\s*#{Bnf::DATE}T#{Bnf::TIME}\s*}.match(v)
137
+ raise Vpim::InvalidEncodingError, "date-time '#{v}' not valid"
138
+ end
139
+ year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
140
+
141
+ [
142
+ # date
143
+ year.to_i, month.to_i, day.to_i,
144
+ # time
145
+ hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
146
+ ]
147
+ end
148
+
149
+ # Vpim.decode_boolean
150
+ #
151
+ # float
152
+ #
153
+ # float_list
154
+ #
155
+ # integer
156
+ #
157
+ # integer_list
158
+ #
159
+ # text_list
160
+
161
+ # Convert a RFC 2425 date-list into an array of dates.
162
+ def Vpim.decode_date_list(v) # :nodoc:
163
+ dates = Vpim.decode_list(v) { |date| Vpim.decode_date(date) }
164
+ end
165
+
166
+ # Convert a RFC 2425 time-list into an array of times.
167
+ def Vpim.decode_time_list(v) # :nodoc:
168
+ times = Vpim.decode_list(v) { |time| Vpim.decode_time(time) }
169
+ end
170
+
171
+ # Convert a RFC 2425 date-time-list into an array of date-times.
172
+ def Vpim.decode_date_time_list(v) # :nodoc:
173
+ datetimes = Vpim.decode_list(v) { |datetime| Vpim.decode_date_time(datetime) }
174
+ end
175
+
176
+ # Convert RFC 2425 text into a String.
177
+ # \\ -> \
178
+ # \n -> NL
179
+ # \N -> NL
180
+ # \, -> ,
181
+ def Vpim.decode_text(v) # :nodoc:
182
+ v.gsub(/\\[nN]/, "\n").gsub(/\\,/, ",").gsub(/\\\\/) { |m| "\\" }
183
+ end
184
+
185
+
186
+ # Unfold the lines in +card+, then return an array of one Field object per
187
+ # line.
188
+ def Vpim.decode(card) #:nodoc:
189
+ content = Vpim.unfold(card).collect { |line| DirectoryInfo::Field.decode(line) }
190
+ end
191
+
192
+
193
+ # Expand an array of fields into its syntactic entities. Each entity is a sequence
194
+ # of fields where the sequences is delimited by a BEGIN/END field. Since
195
+ # BEGIN/END delimited entities can be nested, we build a tree. Each entry in
196
+ # the array is either a Field or an array of entries (where each entry is
197
+ # either a Field, or an array of entries...).
198
+ def Vpim.expand(src) #:nodoc:
199
+ # output array to expand the src to
200
+ dst = []
201
+ # stack used to track our nesting level, as we see begin/end we start a
202
+ # new/finish the current entity, and push/pop that entity from the stack
203
+ current = [ dst ]
204
+
205
+ for f in src
206
+ if f.name? 'begin'
207
+ e = [ f ]
208
+
209
+ current.last.push(e)
210
+ current.push(e)
211
+
212
+ elsif f.name? 'end'
213
+ current.last.push(f)
214
+
215
+ unless current.last.first.value? current.last.last.value
216
+ raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
217
+ end
218
+
219
+ current.pop
220
+
221
+ else
222
+ current.last.push(f)
223
+ end
224
+ end
225
+
226
+ dst
227
+ end
228
+
229
+ # Split an array into an array of all the fields at the outer level, and
230
+ # an array of all the inner arrays of fields. Return the array [outer,
231
+ # inner].
232
+ def Vpim.outer_inner(fields) #:nodoc:
233
+ # seperate into the outer-level fields, and the arrays of component
234
+ # fields
235
+ outer = []
236
+ inner = []
237
+ fields.each do |line|
238
+ case line
239
+ when Array; inner << line
240
+ else; outer << line
241
+ end
242
+ end
243
+ return outer, inner
244
+ end
245
+
246
+ end
247
+