vcard 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,367 @@
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
+ module Vpim
12
+ # Contains regular expression strings for the EBNF of RFC 2425.
13
+ module Bnf #:nodoc:
14
+
15
+ # 1*(ALPHA / DIGIT / "-")
16
+ # Note: I think I can add A-Z here, and get rid of the "i" matches elsewhere.
17
+ # Note: added '_' to allowed because its produced by Notes (X-LOTUS-CHILD_UID:)
18
+ # Note: added '/' to allowed because its produced by KAddressBook (X-messaging/xmpp-All:)
19
+ # Note: added ' ' to allowed because its produced by highrisehq.com (X-GOOGLE TALK:)
20
+ NAME = '[-a-z0-9_/][-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-numzone = sign time-hour [":"] time-minute
57
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
58
+
59
+ # integer = (["+"] / "-") 1*DIGIT
60
+ INTEGER = '[-+]?\d+'
61
+
62
+ # QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII
63
+ # ; Any character except CTLs and DQUOTE
64
+ QSAFECHAR = '[ \t\x21\x23-\x7e\x80-\xff]'
65
+
66
+ # SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII
67
+ # ; Any character except CTLs, DQUOTE, ";", ":", ","
68
+ SAFECHAR = '[ \t\x21\x23-\x2b\x2d-\x39\x3c-\x7e\x80-\xff]'
69
+ end
70
+ end
71
+
72
+ module Vpim
73
+ # Split on \r\n or \n to get the lines, unfold continued lines (they
74
+ # start with ' ' or \t), and return the array of unfolded lines.
75
+ #
76
+ # This also supports the (invalid) encoding convention of allowing empty
77
+ # lines to be inserted for readability - it does this by dropping zero-length
78
+ # lines.
79
+ def Vpim.unfold(card) #:nodoc:
80
+ unfolded = []
81
+
82
+ card.lines do |line|
83
+ line.chomp!
84
+ # If it's a continuation line, add it to the last.
85
+ # If it's an empty line, drop it from the input.
86
+ if( line =~ /^[ \t]/ )
87
+ unfolded[-1] << line[1, line.size-1]
88
+ elsif( line =~ /^$/ )
89
+ else
90
+ unfolded << line
91
+ end
92
+ end
93
+
94
+ unfolded
95
+ end
96
+
97
+ # Convert a +sep+-seperated list of values into an array of values.
98
+ def Vpim.decode_list(value, sep = ',') # :nodoc:
99
+ list = []
100
+
101
+ value.split(sep).each do |item|
102
+ item.chomp!(sep)
103
+ list << yield(item)
104
+ end
105
+ list
106
+ end
107
+
108
+ # Convert a RFC 2425 date into an array of [year, month, day].
109
+ def Vpim.decode_date(v) # :nodoc:
110
+ unless v =~ %r{^\s*#{Bnf::DATE}\s*$}
111
+ raise Vpim::InvalidEncodingError, "date not valid (#{v})"
112
+ end
113
+ [$1.to_i, $2.to_i, $3.to_i]
114
+ end
115
+
116
+ # Convert a RFC 2425 date into a Date object.
117
+ def self.decode_date_to_date(v)
118
+ Date.new(*decode_date(v))
119
+ end
120
+
121
+ # Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445
122
+ # does not. I choose to encode to the subset that is valid for both.
123
+
124
+ # Encode a Date object as "yyyymmdd".
125
+ def Vpim.encode_date(d) # :nodoc:
126
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
127
+ end
128
+
129
+ # Encode a Date object as "yyyymmdd".
130
+ def Vpim.encode_time(d) # :nodoc:
131
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
132
+ end
133
+
134
+ # Encode a Time or DateTime object as "yyyymmddThhmmss"
135
+ def Vpim.encode_date_time(d) # :nodoc:
136
+ "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ]
137
+ end
138
+
139
+ # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
140
+ def Vpim.decode_time(v) # :nodoc:
141
+ unless match = %r{^\s*#{Bnf::TIME}\s*$}.match(v)
142
+ raise Vpim::InvalidEncodingError, "time '#{v}' not valid"
143
+ end
144
+ hour, min, sec, secfrac, tz = match.to_a[1..5]
145
+
146
+ [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
147
+ end
148
+
149
+ def self.array_datetime_to_time(dtarray) #:nodoc:
150
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
151
+ begin
152
+ tz = (dtarray.pop == "Z") ? :gm : :local
153
+ Time.send(tz, *dtarray)
154
+ rescue ArgumentError => e
155
+ raise Vpim::InvalidEncodingError, "#{tz} #{e} (#{dtarray.join(', ')})"
156
+ end
157
+ end
158
+
159
+ # Convert a RFC 2425 time into an array of Time objects.
160
+ def Vpim.decode_time_to_time(v) # :nodoc:
161
+ array_datetime_to_time(decode_date_time(v))
162
+ end
163
+
164
+ # Convert a RFC 2425 date-time into an array of [year,mon,day,hour,min,sec,secfrac,timezone]
165
+ def Vpim.decode_date_time(v) # :nodoc:
166
+ unless match = %r{^\s*#{Bnf::DATE}T#{Bnf::TIME}\s*$}.match(v)
167
+ raise Vpim::InvalidEncodingError, "date-time '#{v}' not valid"
168
+ end
169
+ year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
170
+
171
+ [
172
+ # date
173
+ year.to_i, month.to_i, day.to_i,
174
+ # time
175
+ hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
176
+ ]
177
+ end
178
+
179
+ def Vpim.decode_date_time_to_datetime(v) #:nodoc:
180
+ year, month, day, hour, min, sec, secfrac, tz = Vpim.decode_date_time(v)
181
+ # TODO - DateTime understands timezones, so we could decode tz and use it.
182
+ DateTime.civil(year, month, day, hour, min, sec, 0)
183
+ end
184
+
185
+ # Vpim.decode_boolean
186
+ #
187
+ # float
188
+ #
189
+ # float_list
190
+ =begin
191
+ =end
192
+
193
+ # Convert an RFC2425 INTEGER value into an Integer
194
+ def Vpim.decode_integer(v) # :nodoc:
195
+ unless match = %r{\s*#{Bnf::INTEGER}\s*}.match(v)
196
+ raise Vpim::InvalidEncodingError, "integer not valid (#{v})"
197
+ end
198
+ v.to_i
199
+ end
200
+
201
+ #
202
+ # integer_list
203
+
204
+ # Convert a RFC2425 date-list into an array of dates.
205
+ def Vpim.decode_date_list(v) # :nodoc:
206
+ Vpim.decode_list(v) do |date|
207
+ date.strip!
208
+ if date.length > 0
209
+ Vpim.decode_date(date)
210
+ end
211
+ end.compact
212
+ end
213
+
214
+ # Convert a RFC 2425 time-list into an array of times.
215
+ def Vpim.decode_time_list(v) # :nodoc:
216
+ Vpim.decode_list(v) do |time|
217
+ time.strip!
218
+ if time.length > 0
219
+ Vpim.decode_time(time)
220
+ end
221
+ end.compact
222
+ end
223
+
224
+ # Convert a RFC 2425 date-time-list into an array of date-times.
225
+ def Vpim.decode_date_time_list(v) # :nodoc:
226
+ Vpim.decode_list(v) do |datetime|
227
+ datetime.strip!
228
+ if datetime.length > 0
229
+ Vpim.decode_date_time(datetime)
230
+ end
231
+ end.compact
232
+ end
233
+
234
+ # Convert RFC 2425 text into a String.
235
+ # \\ -> \
236
+ # \n -> NL
237
+ # \N -> NL
238
+ # \, -> ,
239
+ # \; -> ;
240
+ #
241
+ # I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed
242
+ # to escape anything but the above, everything else is ambiguous, so I'll
243
+ # just support it.
244
+ def Vpim.decode_text(v) # :nodoc:
245
+ # FIXME - I think this should trim leading and trailing space
246
+ v.gsub(/\\(.)/) do
247
+ case $1
248
+ when 'n', 'N'
249
+ "\n"
250
+ else
251
+ $1
252
+ end
253
+ end
254
+ end
255
+
256
+ def Vpim.encode_text(v) #:nodoc:
257
+ v.to_str.gsub(/([\\,;\n])/) { $1 == "\n" ? "\\n" : "\\"+$1 }
258
+ end
259
+
260
+ # v is an Array of String, or just a single String
261
+ def Vpim.encode_text_list(v, sep = ",") #:nodoc:
262
+ begin
263
+ v.to_ary.map{ |t| Vpim.encode_text(t) }.join(sep)
264
+ rescue
265
+ Vpim.encode_text(v)
266
+ end
267
+ end
268
+
269
+ # Convert a +sep+-seperated list of TEXT values into an array of values.
270
+ def Vpim.decode_text_list(value, sep = ',') # :nodoc:
271
+ # Need to do in two stages, as best I can find.
272
+ list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map do |v|
273
+ Vpim.decode_text(v.first)
274
+ end
275
+ if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/)
276
+ list << $1
277
+ end
278
+ list
279
+ end
280
+
281
+ # param-value = paramtext / quoted-string
282
+ # paramtext = *SAFE-CHAR
283
+ # quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
284
+ def Vpim.encode_paramtext(value)
285
+ case value
286
+ when %r{\A#{Bnf::SAFECHAR}*\z}
287
+ value
288
+ else
289
+ raise Vpim::Unencodable, "paramtext #{value.inspect}"
290
+ end
291
+ end
292
+
293
+ def Vpim.encode_paramvalue(value)
294
+ case value
295
+ when %r{\A#{Bnf::SAFECHAR}*\z}
296
+ value
297
+ when %r{\A#{Bnf::QSAFECHAR}*\z}
298
+ '"' + value + '"'
299
+ else
300
+ raise Vpim::Unencodable, "param-value #{value.inspect}"
301
+ end
302
+ end
303
+
304
+
305
+ # Unfold the lines in +card+, then return an array of one Field object per
306
+ # line.
307
+ def Vpim.decode(card) #:nodoc:
308
+ content = Vpim.unfold(card).collect { |line| DirectoryInfo::Field.decode(line) }
309
+ end
310
+
311
+
312
+ # Expand an array of fields into its syntactic entities. Each entity is a sequence
313
+ # of fields where the sequences is delimited by a BEGIN/END field. Since
314
+ # BEGIN/END delimited entities can be nested, we build a tree. Each entry in
315
+ # the array is either a Field or an array of entries (where each entry is
316
+ # either a Field, or an array of entries...).
317
+ def Vpim.expand(src) #:nodoc:
318
+ # output array to expand the src to
319
+ dst = []
320
+ # stack used to track our nesting level, as we see begin/end we start a
321
+ # new/finish the current entity, and push/pop that entity from the stack
322
+ current = [ dst ]
323
+
324
+ for f in src
325
+ if f.name? 'BEGIN'
326
+ e = [ f ]
327
+
328
+ current.last.push(e)
329
+ current.push(e)
330
+
331
+ elsif f.name? 'END'
332
+ current.last.push(f)
333
+
334
+ unless current.last.first.value? current.last.last.value
335
+ raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
336
+ end
337
+
338
+ current.pop
339
+
340
+ else
341
+ current.last.push(f)
342
+ end
343
+ end
344
+
345
+ dst
346
+ end
347
+
348
+ # Split an array into an array of all the fields at the outer level, and
349
+ # an array of all the inner arrays of fields. Return the array [outer,
350
+ # inner].
351
+ def Vpim.outer_inner(fields) #:nodoc:
352
+ # TODO - use Enumerable#partition
353
+ # seperate into the outer-level fields, and the arrays of component
354
+ # fields
355
+ outer = []
356
+ inner = []
357
+ fields.each do |line|
358
+ case line
359
+ when Array; inner << line
360
+ else; outer << line
361
+ end
362
+ end
363
+ return outer, inner
364
+ end
365
+
366
+ end
367
+
@@ -0,0 +1,1423 @@
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 'open-uri'
10
+ require 'stringio'
11
+
12
+ module Vpim
13
+ # A vCard, a specialization of a directory info object.
14
+ #
15
+ # The vCard format is specified by:
16
+ # - RFC2426[http://www.ietf.org/rfc/rfc2426.txt]: vCard MIME Directory Profile (vCard 3.0)
17
+ # - RFC2425[http://www.ietf.org/rfc/rfc2425.txt]: A MIME Content-Type for Directory Information
18
+ #
19
+ # This implements vCard 3.0, but it is also capable of working with vCard 2.1
20
+ # if used with care.
21
+ #
22
+ # All line values can be accessed with Vcard#value, Vcard#values, or even by
23
+ # iterating through Vcard#lines. Line types that don't have specific support
24
+ # and non-standard line types ("X-MY-SPECIAL", for example) will be returned
25
+ # as a String, with any base64 or quoted-printable encoding removed.
26
+ #
27
+ # Specific support exists to return more useful values for the standard vCard
28
+ # types, where appropriate.
29
+ #
30
+ # The wrapper functions (#birthday, #nicknames, #emails, etc.) exist
31
+ # partially as an API convenience, and partially as a place to document
32
+ # the values returned for the more complex types, like PHOTO and EMAIL.
33
+ #
34
+ # For types that do not sensibly occur multiple times (like BDAY or GEO),
35
+ # sometimes a wrapper exists only to return a single line, using #value.
36
+ # However, if you find the need, you can still call #values to get all the
37
+ # lines, and both the singular and plural forms will eventually be
38
+ # implemented.
39
+ #
40
+ # For more information see:
41
+ # - RFC2426[http://www.ietf.org/rfc/rfc2426.txt]: vCard MIME Directory Profile (vCard 3.0)
42
+ # - RFC2425[http://www.ietf.org/rfc/rfc2425.txt]: A MIME Content-Type for Directory Information
43
+ # - vCard2.1[http://www.imc.org/pdi/pdiproddev.html]: vCard 2.1 Specifications
44
+ #
45
+ # vCards are usually transmitted in files with <code>.vcf</code>
46
+ # extensions.
47
+ #
48
+ # = Examples
49
+ #
50
+ # - link:ex_mkvcard.txt: example of creating a vCard
51
+ # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
52
+ # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
53
+ # - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards
54
+ # - link:ex_get_vcard_photo.txt: pull photo data from a vCard
55
+ # - link:ab-query.txt: query the OS X Address Book to find vCards
56
+ # - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful
57
+ # with Mutt (see link:README.mutt for details)
58
+ # - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a
59
+ # (small but) complete application contributed by Dane G. Avilla, thanks!
60
+ # - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards
61
+ # - link:vcf-dump.txt: utility for dumping contents of .vcf files
62
+ class Vcard < DirectoryInfo
63
+
64
+ # Represents the value of an ADR field.
65
+ #
66
+ # #location, #preferred, and #delivery indicate information about how the
67
+ # address is to be used, the other attributes are parts of the address.
68
+ #
69
+ # Using values other than those defined for #location or #delivery is
70
+ # unlikely to be portable, or even conformant.
71
+ #
72
+ # All attributes are optional. #location and #delivery can be set to arrays
73
+ # of strings.
74
+ class Address
75
+ # post office box (String)
76
+ attr_accessor :pobox
77
+ # seldom used, its not clear what it is for (String)
78
+ attr_accessor :extended
79
+ # street address (String)
80
+ attr_accessor :street
81
+ # usually the city (String)
82
+ attr_accessor :locality
83
+ # usually the province or state (String)
84
+ attr_accessor :region
85
+ # postal code (String)
86
+ attr_accessor :postalcode
87
+ # country name (String)
88
+ attr_accessor :country
89
+ # home, work (Array of String): the location referred to by the address
90
+ attr_accessor :location
91
+ # true, false (boolean): where this is the preferred address (for this location)
92
+ attr_accessor :preferred
93
+ # postal, parcel, dom (domestic), intl (international) (Array of String): delivery
94
+ # type of this address
95
+ attr_accessor :delivery
96
+
97
+ # nonstandard types, their meaning is undefined (Array of String). These
98
+ # might be found during decoding, but shouldn't be set during encoding.
99
+ attr_reader :nonstandard
100
+
101
+ # Used to simplify some long and tedious code. These symbols are in the
102
+ # order required for the ADR field structured TEXT value, the order
103
+ # cannot be changed.
104
+ @@adr_parts = [
105
+ :@pobox,
106
+ :@extended,
107
+ :@street,
108
+ :@locality,
109
+ :@region,
110
+ :@postalcode,
111
+ :@country,
112
+ ]
113
+
114
+ # TODO
115
+ # - #location?
116
+ # - #delivery?
117
+ def initialize #:nodoc:
118
+ # TODO - Add #label to support LABEL. Try to find LABEL
119
+ # in either same group, or with sam params.
120
+ @@adr_parts.each do |part|
121
+ instance_variable_set(part, '')
122
+ end
123
+
124
+ @location = []
125
+ @preferred = false
126
+ @delivery = []
127
+ @nonstandard = []
128
+ end
129
+
130
+ def encode #:nodoc:
131
+ parts = @@adr_parts.map do |part|
132
+ instance_variable_get(part)
133
+ end
134
+
135
+ value = Vpim.encode_text_list(parts, ";")
136
+
137
+ params = [ @location, @delivery, @nonstandard ]
138
+ params << 'pref' if @preferred
139
+ params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
140
+
141
+ paramshash = {}
142
+
143
+ paramshash['TYPE'] = params if params.first
144
+
145
+ Vpim::DirectoryInfo::Field.create( 'ADR', value, paramshash)
146
+ end
147
+
148
+ def Address.decode(card, field) #:nodoc:
149
+ adr = new
150
+
151
+ parts = Vpim.decode_text_list(field.value_raw, ';')
152
+
153
+ @@adr_parts.each_with_index do |part,i|
154
+ adr.instance_variable_set(part, parts[i] || '')
155
+ end
156
+
157
+ params = field.pvalues('TYPE')
158
+
159
+ if params
160
+ params.each do |p|
161
+ p.downcase!
162
+ case p
163
+ when 'home', 'work'
164
+ adr.location << p
165
+ when 'postal', 'parcel', 'dom', 'intl'
166
+ adr.delivery << p
167
+ when 'pref'
168
+ adr.preferred = true
169
+ else
170
+ adr.nonstandard << p
171
+ end
172
+ end
173
+ # Strip duplicates
174
+ [ adr.location, adr.delivery, adr.nonstandard ].each do |a|
175
+ a.uniq!
176
+ end
177
+ end
178
+
179
+ adr
180
+ end
181
+ end
182
+
183
+ # Represents the value of an EMAIL field.
184
+ class Email < String
185
+ # true, false (boolean): whether this is the preferred email address
186
+ attr_accessor :preferred
187
+ # internet, x400 (String): the email address format, rarely specified
188
+ # since the default is 'internet'
189
+ attr_accessor :format
190
+ # home, work (Array of String): the location referred to by the address. The
191
+ # inclusion of location parameters in a vCard seems to be non-conformant,
192
+ # strictly speaking, but also seems to be widespread.
193
+ attr_accessor :location
194
+ # nonstandard types, their meaning is undefined (Array of String). These
195
+ # might be found during decoding, but shouldn't be set during encoding.
196
+ attr_reader :nonstandard
197
+
198
+ def initialize(email='') #:nodoc:
199
+ @preferred = false
200
+ @format = 'internet'
201
+ @location = []
202
+ @nonstandard = []
203
+ super(email)
204
+ end
205
+
206
+ def inspect #:nodoc:
207
+ s = "#<#{self.class.to_s}: #{to_str.inspect}"
208
+ s << ", pref" if preferred
209
+ s << ", #{format}" if format != 'internet'
210
+ s << ", " << @location.join(", ") if @location.first
211
+ s << ", #{@nonstandard.join(", ")}" if @nonstandard.first
212
+ s
213
+ end
214
+
215
+ def encode #:nodoc:
216
+ value = to_str.strip
217
+
218
+ if value.length < 1
219
+ raise InvalidEncodingError, "EMAIL must have a value"
220
+ end
221
+
222
+ params = [ @location, @nonstandard ]
223
+ params << @format if @format != 'internet'
224
+ params << 'pref' if @preferred
225
+
226
+ params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
227
+
228
+ paramshash = {}
229
+
230
+ paramshash['TYPE'] = params if params.first
231
+
232
+ Vpim::DirectoryInfo::Field.create( 'EMAIL', value, paramshash)
233
+ end
234
+
235
+ def Email.decode(field) #:nodoc:
236
+ value = field.to_text.strip
237
+
238
+ if value.length < 1
239
+ raise InvalidEncodingError, "EMAIL must have a value"
240
+ end
241
+
242
+ eml = Email.new(value)
243
+
244
+ params = field.pvalues('TYPE')
245
+
246
+ if params
247
+ params.each do |p|
248
+ p.downcase!
249
+ case p
250
+ when 'home', 'work'
251
+ eml.location << p
252
+ when 'pref'
253
+ eml.preferred = true
254
+ when 'x400', 'internet'
255
+ eml.format = p
256
+ else
257
+ eml.nonstandard << p
258
+ end
259
+ end
260
+ # Strip duplicates
261
+ [ eml.location, eml.nonstandard ].each do |a|
262
+ a.uniq!
263
+ end
264
+ end
265
+
266
+ eml
267
+ end
268
+ end
269
+
270
+ # Represents the value of a TEL field.
271
+ #
272
+ # The value is supposed to be a "X.500 Telephone Number" according to RFC
273
+ # 2426, but that standard is not freely available. Otherwise, anything that
274
+ # looks like a phone number should be OK.
275
+ class Telephone < String
276
+ # true, false (boolean): whether this is the preferred email address
277
+ attr_accessor :preferred
278
+ # home, work, cell, car, pager (Array of String): the location
279
+ # of the device
280
+ attr_accessor :location
281
+ # voice, fax, video, msg, bbs, modem, isdn, pcs (Array of String): the
282
+ # capabilities of the device
283
+ attr_accessor :capability
284
+ # nonstandard types, their meaning is undefined (Array of String). These
285
+ # might be found during decoding, but shouldn't be set during encoding.
286
+ attr_reader :nonstandard
287
+
288
+ def initialize(telephone='') #:nodoc:
289
+ @preferred = false
290
+ @location = []
291
+ @capability = []
292
+ @nonstandard = []
293
+ super(telephone)
294
+ end
295
+
296
+ def inspect #:nodoc:
297
+ s = "#<#{self.class.to_s}: #{to_str.inspect}"
298
+ s << ", pref" if preferred
299
+ s << ", " << @location.join(", ") if @location.first
300
+ s << ", " << @capability.join(", ") if @capability.first
301
+ s << ", #{@nonstandard.join(", ")}" if @nonstandard.first
302
+ s
303
+ end
304
+
305
+ def encode #:nodoc:
306
+ value = to_str.strip
307
+
308
+ if value.length < 1
309
+ raise InvalidEncodingError, "TEL must have a value"
310
+ end
311
+
312
+ params = [ @location, @capability, @nonstandard ]
313
+ params << 'pref' if @preferred
314
+
315
+ params = params.flatten.compact.map { |s| s.to_str.downcase }.uniq
316
+
317
+ paramshash = {}
318
+
319
+ paramshash['TYPE'] = params if params.first
320
+
321
+ Vpim::DirectoryInfo::Field.create( 'TEL', value, paramshash)
322
+ end
323
+
324
+ def Telephone.decode(field) #:nodoc:
325
+ value = field.to_text.strip
326
+
327
+ if value.length < 1
328
+ raise InvalidEncodingError, "TEL must have a value"
329
+ end
330
+
331
+ tel = Telephone.new(value)
332
+
333
+ params = field.pvalues('TYPE')
334
+
335
+ if params
336
+ params.each do |p|
337
+ p.downcase!
338
+ case p
339
+ when 'home', 'work', 'cell', 'car', 'pager'
340
+ tel.location << p
341
+ when 'voice', 'fax', 'video', 'msg', 'bbs', 'modem', 'isdn', 'pcs'
342
+ tel.capability << p
343
+ when 'pref'
344
+ tel.preferred = true
345
+ else
346
+ tel.nonstandard << p
347
+ end
348
+ end
349
+ # Strip duplicates
350
+ [ tel.location, tel.capability, tel.nonstandard ].each do |a|
351
+ a.uniq!
352
+ end
353
+ end
354
+
355
+ tel
356
+ end
357
+ end
358
+
359
+ # The name from a vCard, including all the components of the N: and FN:
360
+ # fields.
361
+ class Name
362
+ # family name, from N
363
+ attr_accessor :family
364
+ # given name, from N
365
+ attr_accessor :given
366
+ # additional names, from N
367
+ attr_accessor :additional
368
+ # such as "Ms." or "Dr.", from N
369
+ attr_accessor :prefix
370
+ # such as "BFA", from N
371
+ attr_accessor :suffix
372
+ # full name, the FN field. FN is a formatted version of the N field,
373
+ # intended to be in a form more aligned with the cultural conventions of
374
+ # the vCard owner than +formatted+ is.
375
+ attr_accessor :fullname
376
+ # all the components of N formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}"
377
+ attr_reader :formatted
378
+
379
+ # Override the attr reader to make it dynamic
380
+ remove_method :formatted
381
+ def formatted #:nodoc:
382
+ f = [ @prefix, @given, @additional, @family ].map{|i| i == '' ? nil : i.strip}.compact.join(' ')
383
+ if @suffix != ''
384
+ f << ', ' << @suffix
385
+ end
386
+ f
387
+ end
388
+
389
+ def initialize(n='', fn='') #:nodoc:
390
+ n = Vpim.decode_text_list(n, ';') do |item|
391
+ item.strip
392
+ end
393
+
394
+ @family = n[0] || ""
395
+ @given = n[1] || ""
396
+ @additional = n[2] || ""
397
+ @prefix = n[3] || ""
398
+ @suffix = n[4] || ""
399
+
400
+ # FIXME - make calls to #fullname fail if fn is nil
401
+ @fullname = (fn || "").strip
402
+ end
403
+
404
+ def encode #:nodoc:
405
+ Vpim::DirectoryInfo::Field.create('N',
406
+ Vpim.encode_text_list([ @family, @given, @additional, @prefix, @suffix ].map{|n| n.strip}, ';')
407
+ )
408
+ end
409
+ def encode_fn #:nodoc:
410
+ fn = @fullname.strip
411
+ if @fullname.length == 0
412
+ fn = formatted
413
+ end
414
+ Vpim::DirectoryInfo::Field.create('FN', fn)
415
+ end
416
+
417
+ end
418
+
419
+ def decode_invisible(field) #:nodoc:
420
+ nil
421
+ end
422
+
423
+ def decode_default(field) #:nodoc:
424
+ Line.new( field.group, field.name, field.value )
425
+ end
426
+
427
+ def decode_version(field) #:nodoc:
428
+ Line.new( field.group, field.name, (field.value.to_f * 10).to_i )
429
+ end
430
+
431
+ def decode_text(field) #:nodoc:
432
+ Line.new( field.group, field.name, Vpim.decode_text(field.value_raw) )
433
+ end
434
+
435
+ def decode_n(field) #:nodoc:
436
+ Line.new( field.group, field.name, Name.new(field.value, self['FN']).freeze )
437
+ end
438
+
439
+ def decode_date_or_datetime(field) #:nodoc:
440
+ date = nil
441
+ begin
442
+ date = Vpim.decode_date_to_date(field.value_raw)
443
+ rescue Vpim::InvalidEncodingError
444
+ date = Vpim.decode_date_time_to_datetime(field.value_raw)
445
+ end
446
+ Line.new( field.group, field.name, date )
447
+ end
448
+
449
+ def decode_bday(field) #:nodoc:
450
+ begin
451
+ return decode_date_or_datetime(field)
452
+
453
+ rescue Vpim::InvalidEncodingError
454
+ # Hack around BDAY dates hat are correct in the month and day, but have
455
+ # some kind of garbage in the year.
456
+ if field.value =~ /^\s*(\d+)-(\d+)-(\d+)\s*$/
457
+ y = $1.to_i
458
+ m = $2.to_i
459
+ d = $3.to_i
460
+ if(y < 1900)
461
+ y = Time.now.year
462
+ end
463
+ Line.new( field.group, field.name, Date.new(y, m, d) )
464
+ else
465
+ raise
466
+ end
467
+ end
468
+ end
469
+
470
+ def decode_geo(field) #:nodoc:
471
+ geo = Vpim.decode_list(field.value_raw, ';') do |item| item.to_f end
472
+ Line.new( field.group, field.name, geo )
473
+ end
474
+
475
+ def decode_address(field) #:nodoc:
476
+ Line.new( field.group, field.name, Address.decode(self, field) )
477
+ end
478
+
479
+ def decode_email(field) #:nodoc:
480
+ Line.new( field.group, field.name, Email.decode(field) )
481
+ end
482
+
483
+ def decode_telephone(field) #:nodoc:
484
+ Line.new( field.group, field.name, Telephone.decode(field) )
485
+ end
486
+
487
+ def decode_list_of_text(field) #:nodoc:
488
+ Line.new( field.group, field.name,
489
+ Vpim.decode_text_list(field.value_raw).select{|t| t.length > 0}.uniq
490
+ )
491
+ end
492
+
493
+ def decode_structured_text(field) #:nodoc:
494
+ Line.new( field.group, field.name, Vpim.decode_text_list(field.value_raw, ';') )
495
+ end
496
+
497
+ def decode_uri(field) #:nodoc:
498
+ Line.new( field.group, field.name, Attachment::Uri.new(field.value, nil) )
499
+ end
500
+
501
+ def decode_agent(field) #:nodoc:
502
+ case field.kind
503
+ when 'text'
504
+ decode_text(field)
505
+ when 'uri'
506
+ decode_uri(field)
507
+ when 'vcard', nil
508
+ Line.new( field.group, field.name, Vcard.decode(Vpim.decode_text(field.value_raw)).first )
509
+ else
510
+ raise InvalidEncodingError, "AGENT type #{field.kind} is not allowed"
511
+ end
512
+ end
513
+
514
+ def decode_attachment(field) #:nodoc:
515
+ Line.new( field.group, field.name, Attachment.decode(field, 'binary', 'TYPE') )
516
+ end
517
+
518
+ @@decode = {
519
+ 'BEGIN' => :decode_invisible, # Don't return delimiter
520
+ 'END' => :decode_invisible, # Don't return delimiter
521
+ 'FN' => :decode_invisible, # Returned as part of N.
522
+
523
+ 'ADR' => :decode_address,
524
+ 'AGENT' => :decode_agent,
525
+ 'BDAY' => :decode_bday,
526
+ 'CATEGORIES' => :decode_list_of_text,
527
+ 'EMAIL' => :decode_email,
528
+ 'GEO' => :decode_geo,
529
+ 'KEY' => :decode_attachment,
530
+ 'LOGO' => :decode_attachment,
531
+ 'MAILER' => :decode_text,
532
+ 'N' => :decode_n,
533
+ 'NAME' => :decode_text,
534
+ 'NICKNAME' => :decode_list_of_text,
535
+ 'NOTE' => :decode_text,
536
+ 'ORG' => :decode_structured_text,
537
+ 'PHOTO' => :decode_attachment,
538
+ 'PRODID' => :decode_text,
539
+ 'PROFILE' => :decode_text,
540
+ 'REV' => :decode_date_or_datetime,
541
+ 'ROLE' => :decode_text,
542
+ 'SOUND' => :decode_attachment,
543
+ 'SOURCE' => :decode_text,
544
+ 'TEL' => :decode_telephone,
545
+ 'TITLE' => :decode_text,
546
+ 'UID' => :decode_text,
547
+ 'URL' => :decode_uri,
548
+ 'VERSION' => :decode_version,
549
+ }
550
+
551
+ @@decode.default = :decode_default
552
+
553
+ # Cache of decoded lines/fields, so we don't have to decode a field more than once.
554
+ attr_reader :cache #:nodoc:
555
+
556
+ # An entry in a vCard. The #value object's type varies with the kind of
557
+ # line (the #name), and on how the line was encoded. The objects returned
558
+ # for a specific kind of line are often extended so that they support a
559
+ # common set of methods. The goal is to allow all types of objects for a
560
+ # kind of line to be treated with some uniformity, but still allow specific
561
+ # handling for the various value types if desired.
562
+ #
563
+ # See the specific methods for details.
564
+ class Line
565
+ attr_reader :group
566
+ attr_reader :name
567
+ attr_reader :value
568
+
569
+ def initialize(group, name, value) #:nodoc:
570
+ @group, @name, @value = (group||''), name.to_str, value
571
+ end
572
+
573
+ def self.decode(decode, card, field) #:nodoc:
574
+ card.cache[field] || (card.cache[field] = card.send(decode[field.name], field))
575
+ end
576
+ end
577
+
578
+ #@lines = {} FIXME - dead code
579
+
580
+ # Return line for a field
581
+ def f2l(field) #:nodoc:
582
+ begin
583
+ Line.decode(@@decode, self, field)
584
+ rescue InvalidEncodingError
585
+ # Skip invalidly encoded fields.
586
+ end
587
+ end
588
+
589
+ # With no block, returns an Array of Line. If +name+ is specified, the
590
+ # Array will only contain the +Line+s with that +name+. The Array may be
591
+ # empty.
592
+ #
593
+ # If a block is given, each Line will be yielded instead of being returned
594
+ # in an Array.
595
+ def lines(name=nil) #:yield: Line
596
+ # FIXME - this would be much easier if #lines was #each, and there was a
597
+ # different #lines that returned an Enumerator that used #each
598
+ unless block_given?
599
+ map do |f|
600
+ if( !name || f.name?(name) )
601
+ f2l(f)
602
+ else
603
+ nil
604
+ end
605
+ end.compact
606
+ else
607
+ each do |f|
608
+ if( !name || f.name?(name) )
609
+ line = f2l(f)
610
+ if line
611
+ yield line
612
+ end
613
+ end
614
+ end
615
+ self
616
+ end
617
+ end
618
+
619
+ private_class_method :new
620
+
621
+ def initialize(fields, profile) #:nodoc:
622
+ @cache = {}
623
+ super(fields, profile)
624
+ end
625
+
626
+ # Create a vCard 3.0 object with the minimum required fields, plus any
627
+ # +fields+ you want in the card (they can also be added later).
628
+ def Vcard.create(fields = [] )
629
+ fields.unshift Field.create('VERSION', "3.0")
630
+ super(fields, 'VCARD')
631
+ end
632
+
633
+ # Decode a collection of vCards into an array of Vcard objects.
634
+ #
635
+ # +card+ can be either a String or an IO object.
636
+ #
637
+ # Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard),
638
+ # multiple vCards can be concatenated into a single directory info object.
639
+ # They may or may not be related. For example, AddressBook.app (the OS X
640
+ # contact manager) will export multiple selected cards in this format.
641
+ #
642
+ # Input data will be converted from unicode if it is detected. The heuristic
643
+ # is based on the first bytes in the string:
644
+ # - 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped
645
+ # - 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string
646
+ # is converted to UTF-8
647
+ # - 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string
648
+ # is converted to UTF-8
649
+ # - 0x00 'B' or 0x00 'b': UTF-16 (big-endian), the string is converted to UTF-8
650
+ # - 'B' 0x00 or 'b' 0x00: UTF-16 (little-endian), the string is converted to UTF-8
651
+ #
652
+ # If you know that you have only one vCard, then you can decode that
653
+ # single vCard by doing something like:
654
+ #
655
+ # vcard = Vcard.decode(card_data).first
656
+ #
657
+ # Note: Should the import encoding be remembered, so that it can be reencoded in
658
+ # the same format?
659
+ def Vcard.decode(card)
660
+ if card.respond_to? :to_str
661
+ string = card.to_str
662
+ elsif card.respond_to? :read
663
+ string = card.read(nil)
664
+ else
665
+ raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
666
+ end
667
+
668
+ case string
669
+ when /^\xEF\xBB\xBF/
670
+ string = string.sub("\xEF\xBB\xBF", '')
671
+ when /^\xFE\xFF/
672
+ arr = string.unpack('n*')
673
+ arr.shift
674
+ string = arr.pack('U*')
675
+ when /^\xFF\xFE/
676
+ arr = string.unpack('v*')
677
+ arr.shift
678
+ string = arr.pack('U*')
679
+ when /^\x00B/i
680
+ string = string.unpack('n*').pack('U*')
681
+ when /^B\x00/i
682
+ string = string.unpack('v*').pack('U*')
683
+ end
684
+
685
+ string.force_encoding(Encoding::UTF_8)
686
+ entities = Vpim.expand(Vpim.decode(string))
687
+
688
+ # Since all vCards must have a begin/end, the top-level should consist
689
+ # entirely of entities/arrays, even if its a single vCard.
690
+ if entities.detect { |e| ! e.kind_of? Array }
691
+ raise "Not a valid vCard"
692
+ end
693
+
694
+ vcards = []
695
+
696
+ for e in entities
697
+ vcards.push(new(e.flatten, 'VCARD'))
698
+ end
699
+
700
+ vcards
701
+ end
702
+
703
+ # The value of the field named +name+, optionally limited to fields of
704
+ # type +type+. If no match is found, nil is returned, if multiple matches
705
+ # are found, the first match to have one of its type values be 'PREF'
706
+ # (preferred) is returned, otherwise the first match is returned.
707
+ #
708
+ # FIXME - this will become an alias for #value.
709
+ def [](name, type=nil)
710
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
711
+
712
+ valued = fields.select { |f| f.value != '' }
713
+ if valued.first
714
+ fields = valued
715
+ end
716
+
717
+ # limit to preferred, if possible
718
+ pref = fields.select { |f| f.pref? }
719
+
720
+ if pref.first
721
+ fields = pref
722
+ end
723
+
724
+ fields.first ? fields.first.value : nil
725
+ end
726
+
727
+ # Return the Line#value for a specific +name+, and optionally for a
728
+ # specific +type+.
729
+ #
730
+ # If no line with the +name+ (and, optionally, +type+) exists, nil is
731
+ # returned.
732
+ #
733
+ # If multiple lines exist, the order of preference is:
734
+ # - lines with values over lines without
735
+ # - lines with a type of 'pref' over lines without
736
+ # If multiple lines are equally preferred, then the first line will be
737
+ # returned.
738
+ #
739
+ # This is most useful when looking for a line that can not occur multiple
740
+ # times, or when the line can occur multiple times, and you want to pick
741
+ # the first preferred line of a specific type. See #values if you need to
742
+ # access all the lines.
743
+ #
744
+ # Note that the +type+ field parameter is used for different purposes by
745
+ # the various kinds of vCard lines, but for the addressing lines (ADR,
746
+ # LABEL, TEL, EMAIL) it is has a reasonably consistent usage. Each
747
+ # addressing line can occur multiple times, and a +type+ of 'pref'
748
+ # indicates that a particular line is the preferred line. Other +type+
749
+ # values tend to indicate some information about the location ('home',
750
+ # 'work', ...) or some detail about the address ('cell', 'fax', 'voice',
751
+ # ...). See the methods for the specific types of line for information
752
+ # about supported types and their meaning.
753
+ def value(name, type = nil)
754
+ v = nil
755
+
756
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
757
+
758
+ valued = fields.select { |f| f.value != '' }
759
+ if valued.first
760
+ fields = valued
761
+ end
762
+
763
+ pref = fields.select { |f| f.pref? }
764
+
765
+ if pref.first
766
+ fields = pref
767
+ end
768
+
769
+ if fields.first
770
+ line = begin
771
+ Line.decode(@@decode, self, fields.first)
772
+ rescue Vpim::InvalidEncodingError
773
+ end
774
+
775
+ if line
776
+ return line.value
777
+ end
778
+ end
779
+
780
+ nil
781
+ end
782
+
783
+ # A variant of #lines that only iterates over specific Line names. Since
784
+ # the name is known, only the Line#value is returned or yielded.
785
+ def values(name)
786
+ unless block_given?
787
+ lines(name).map { |line| line.value }
788
+ else
789
+ lines(name) { |line| yield line.value }
790
+ end
791
+ end
792
+
793
+ # The first ADR value of type +type+, a Address. Any of the location or
794
+ # delivery attributes of Address can be used as +type+. A wrapper around
795
+ # #value('ADR', +type+).
796
+ def address(type=nil)
797
+ value('ADR', type)
798
+ end
799
+
800
+ # The ADR values, an array of Address. If a block is given, the values are
801
+ # yielded. A wrapper around #values('ADR').
802
+ def addresses #:yield:address
803
+ values('ADR')
804
+ end
805
+
806
+ # The AGENT values. Each AGENT value is either a String, a Uri, or a Vcard.
807
+ # If a block is given, the values are yielded. A wrapper around
808
+ # #values('AGENT').
809
+ def agents #:yield:agent
810
+ values('AGENT')
811
+ end
812
+
813
+ # The BDAY value as either a Date or a DateTime, or nil if there is none.
814
+ #
815
+ # If the BDAY value is invalidly formatted, a feeble heuristic is applied
816
+ # to find the month and year, and return a Date in the current year.
817
+ def birthday
818
+ value('BDAY')
819
+ end
820
+
821
+ # The CATEGORIES values, an array of String. A wrapper around
822
+ # #value('CATEGORIES').
823
+ def categories
824
+ value('CATEGORIES')
825
+ end
826
+
827
+ # The first EMAIL value of type +type+, a Email. Any of the location
828
+ # attributes of Email can be used as +type+. A wrapper around
829
+ # #value('EMAIL', +type+).
830
+ def email(type=nil)
831
+ value('EMAIL', type)
832
+ end
833
+
834
+ # The EMAIL values, an array of Email. If a block is given, the values are
835
+ # yielded. A wrapper around #values('EMAIL').
836
+ def emails #:yield:email
837
+ values('EMAIL')
838
+ end
839
+
840
+ # The GEO value, an Array of two Floats, +[ latitude, longitude]+. North
841
+ # of the equator is positive latitude, east of the meridian is positive
842
+ # longitude. See RFC2445 for more info, there are lots of special cases
843
+ # and RFC2445's description is more complete thant RFC2426.
844
+ def geo
845
+ value('GEO')
846
+ end
847
+
848
+ # Return an Array of KEY Line#value, or yield each Line#value if a block
849
+ # is given. A wrapper around #values('KEY').
850
+ #
851
+ # KEY is a public key or authentication certificate associated with the
852
+ # object that the vCard represents. It is not commonly used, but could
853
+ # contain a X.509 or PGP certificate.
854
+ #
855
+ # See Attachment for a description of the value.
856
+ def keys(&proc) #:yield: Line.value
857
+ values('KEY', &proc)
858
+ end
859
+
860
+ # Return an Array of LOGO Line#value, or yield each Line#value if a block
861
+ # is given. A wrapper around #values('LOGO').
862
+ #
863
+ # LOGO is a graphic image of a logo associated with the object the vCard
864
+ # represents. Its not common, but would probably be equivalent to the logo
865
+ # on a printed card.
866
+ #
867
+ # See Attachment for a description of the value.
868
+ def logos(&proc) #:yield: Line.value
869
+ values('LOGO', &proc)
870
+ end
871
+
872
+ ## MAILER
873
+
874
+ # The N and FN as a Name object.
875
+ #
876
+ # N is required for a vCards, this raises InvalidEncodingError if
877
+ # there is no N so it cannot return nil.
878
+ def name
879
+ value('N') || raise(Vpim::InvalidEncodingError, "Missing mandatory N field")
880
+ end
881
+
882
+ # The first NICKNAME value, nil if there are none.
883
+ def nickname
884
+ v = value('NICKNAME')
885
+ v = v.first if v
886
+ v
887
+ end
888
+
889
+ # The NICKNAME values, an array of String. The array may be empty.
890
+ def nicknames
891
+ values('NICKNAME').flatten.uniq
892
+ end
893
+
894
+ # The NOTE value, a String. A wrapper around #value('NOTE').
895
+ def note
896
+ value('NOTE')
897
+ end
898
+
899
+ # The ORG value, an Array of String. The first string is the organization,
900
+ # subsequent strings are departments within the organization. A wrapper
901
+ # around #value('ORG').
902
+ def org
903
+ value('ORG')
904
+ end
905
+
906
+ # Return an Array of PHOTO Line#value, or yield each Line#value if a block
907
+ # is given. A wrapper around #values('PHOTO').
908
+ #
909
+ # PHOTO is an image or photograph information that annotates some aspect of
910
+ # the object the vCard represents. Commonly there is one PHOTO, and it is a
911
+ # photo of the person identified by the vCard.
912
+ #
913
+ # See Attachment for a description of the value.
914
+ def photos(&proc) #:yield: Line.value
915
+ values('PHOTO', &proc)
916
+ end
917
+
918
+ ## PRODID
919
+
920
+ ## PROFILE
921
+
922
+ ## REV
923
+
924
+ ## ROLE
925
+
926
+ # Return an Array of SOUND Line#value, or yield each Line#value if a block
927
+ # is given. A wrapper around #values('SOUND').
928
+ #
929
+ # SOUND is digital sound content information that annotates some aspect of
930
+ # the vCard. By default this type is used to specify the proper
931
+ # pronunciation of the name associated with the vCard. It is not commonly
932
+ # used. Also, note that there is no mechanism available to specify that the
933
+ # SOUND is being used for anything other than the default.
934
+ #
935
+ # See Attachment for a description of the value.
936
+ def sounds(&proc) #:yield: Line.value
937
+ values('SOUND', &proc)
938
+ end
939
+
940
+ ## SOURCE
941
+
942
+ # The first TEL value of type +type+, a Telephone. Any of the location or
943
+ # capability attributes of Telephone can be used as +type+. A wrapper around
944
+ # #value('TEL', +type+).
945
+ def telephone(type=nil)
946
+ value('TEL', type)
947
+ end
948
+
949
+ # The TEL values, an array of Telephone. If a block is given, the values are
950
+ # yielded. A wrapper around #values('TEL').
951
+ def telephones #:yield:tel
952
+ values('TEL')
953
+ end
954
+
955
+ # The TITLE value, a text string specifying the job title, functional
956
+ # position, or function of the object the card represents. A wrapper around
957
+ # #value('TITLE').
958
+ def title
959
+ value('TITLE')
960
+ end
961
+
962
+ ## UID
963
+
964
+ # The URL value, a Attachment::Uri. A wrapper around #value('URL').
965
+ def url
966
+ value('URL')
967
+ end
968
+
969
+ # The URL values, an Attachment::Uri. A wrapper around #values('URL').
970
+ def urls
971
+ values('URL')
972
+ end
973
+
974
+ # The VERSION multiplied by 10 as an Integer. For example, a VERSION:2.1
975
+ # vCard would have a version of 21, and a VERSION:3.0 vCard would have a
976
+ # version of 30.
977
+ #
978
+ # VERSION is required for a vCard, this raises InvalidEncodingError if
979
+ # there is no VERSION so it cannot return nil.
980
+ def version
981
+ v = value('VERSION')
982
+ unless v
983
+ raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
984
+ end
985
+ v
986
+ end
987
+
988
+ # Make changes to a vCard.
989
+ #
990
+ # Yields a Vpim::Vcard::Maker that can be used to modify this vCard.
991
+ def make #:yield: maker
992
+ Vpim::Vcard::Maker.make2(self) do |maker|
993
+ yield maker
994
+ end
995
+ end
996
+
997
+ # Delete +line+ if block yields true.
998
+ def delete_if #:nodoc: :yield: line
999
+ # Do in two steps to not mess up progress through the enumerator.
1000
+ rm = []
1001
+
1002
+ each do |f|
1003
+ line = f2l(f)
1004
+ if line && yield(line)
1005
+ rm << f
1006
+
1007
+ # Hack - because we treat N and FN as one field
1008
+ if f.name? 'N'
1009
+ rm << field('FN')
1010
+ end
1011
+ end
1012
+ end
1013
+
1014
+ rm.each do |f|
1015
+ @fields.delete( f )
1016
+ @cache.delete( f )
1017
+ end
1018
+
1019
+ end
1020
+
1021
+ # A class to make and make changes to vCards.
1022
+ #
1023
+ # It can be used to create completely new vCards using Vcard#make2.
1024
+ #
1025
+ # Its is also yielded from Vpim::Vcard#make, in which case it allows a kind
1026
+ # of transactional approach to changing vCards, so their values can be
1027
+ # validated after any changes have been made.
1028
+ #
1029
+ # Examples:
1030
+ # - link:ex_mkvcard.txt: example of creating a vCard
1031
+ # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
1032
+ # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
1033
+ # - link:ex_mkyourown.txt: example of adding support for new fields to Vcard::Maker
1034
+ class Maker
1035
+ # Make a vCard.
1036
+ #
1037
+ # Yields +maker+, a Vpim::Vcard::Maker which allows fields to be added to
1038
+ # +card+, and returns +card+, a Vpim::Vcard.
1039
+ #
1040
+ # If +card+ is nil or not provided a new Vpim::Vcard is created and the
1041
+ # fields are added to it.
1042
+ #
1043
+ # Defaults:
1044
+ # - vCards must have both an N and an FN field, #make2 will fail if there
1045
+ # is no N field in the +card+ when your block is finished adding fields.
1046
+ # - If there is an N field, but no FN field, FN will be set from the
1047
+ # information in N, see Vcard::Name#preformatted for more information.
1048
+ # - vCards must have a VERSION field. If one does not exist when your block is
1049
+ # is finished it will be set to 3.0.
1050
+ def self.make2(card = Vpim::Vcard.create, &block) # :yields: maker
1051
+ new(nil, card).make(&block)
1052
+ end
1053
+
1054
+ # Deprecated, use #make2.
1055
+ #
1056
+ # If set, the FN field will be set to +full_name+. Otherwise, FN will
1057
+ # be set from the values in #name.
1058
+ def self.make(full_name = nil, &block) # :yields: maker
1059
+ new(full_name, Vpim::Vcard.create).make(&block)
1060
+ end
1061
+
1062
+ def make # :nodoc:
1063
+ yield self
1064
+ unless @card['N']
1065
+ raise Unencodeable, 'N field is mandatory'
1066
+ end
1067
+ fn = @card.field('FN')
1068
+ if fn && fn.value.strip.length == 0
1069
+ @card.delete(fn)
1070
+ fn = nil
1071
+ end
1072
+ unless fn
1073
+ @card << Vpim::DirectoryInfo::Field.create('FN', Vpim::Vcard::Name.new(@card['N'], '').formatted)
1074
+ end
1075
+ unless @card['VERSION']
1076
+ @card << Vpim::DirectoryInfo::Field.create('VERSION', "3.0")
1077
+ end
1078
+ @card
1079
+ end
1080
+
1081
+ private
1082
+
1083
+ def initialize(full_name, card) # :nodoc:
1084
+ @card = card || Vpim::Vcard::create
1085
+ if full_name
1086
+ @card << Vpim::DirectoryInfo::Field.create('FN', full_name.strip )
1087
+ end
1088
+ end
1089
+
1090
+ public
1091
+
1092
+ # Deprecated, see #name.
1093
+ #
1094
+ # Use
1095
+ # maker.name do |n| n.fullname = "foo" end
1096
+ # to set just fullname, or set the other fields to set fullname and the
1097
+ # name.
1098
+ def fullname=(fullname) #:nodoc: bacwards compat
1099
+ if @card.field('FN')
1100
+ raise Vpim::InvalidEncodingError, "Not allowed to add more than one FN field to a vCard."
1101
+ end
1102
+ @card << Vpim::DirectoryInfo::Field.create( 'FN', fullname );
1103
+ end
1104
+
1105
+ # Set the name fields, N and FN.
1106
+ #
1107
+ # Attributes of +name+ are:
1108
+ # - family: family name
1109
+ # - given: given name
1110
+ # - additional: additional names
1111
+ # - prefix: such as "Ms." or "Dr."
1112
+ # - suffix: such as "BFA", or "Sensei"
1113
+ #
1114
+ # +name+ is a Vcard::Name.
1115
+ #
1116
+ # All attributes are optional, though have all names be zero-length
1117
+ # strings isn't really in the spirit of things. FN's value will be set
1118
+ # to Vcard::Name#formatted if Vcard::Name#fullname isn't given a specific
1119
+ # value.
1120
+ #
1121
+ # Warning: This is the only mandatory field.
1122
+ def name #:yield:name
1123
+ x = begin
1124
+ @card.name.dup
1125
+ rescue
1126
+ Vpim::Vcard::Name.new
1127
+ end
1128
+
1129
+ fn = x.fullname
1130
+
1131
+ yield x
1132
+
1133
+ x.fullname.strip!
1134
+
1135
+ delete_if do |line|
1136
+ line.name == 'N'
1137
+ end
1138
+
1139
+ @card << x.encode
1140
+ @card << x.encode_fn
1141
+
1142
+ self
1143
+ end
1144
+
1145
+ alias :add_name :name #:nodoc: backwards compatibility
1146
+
1147
+ # Add an address field, ADR. +address+ is a Vpim::Vcard::Address.
1148
+ def add_addr # :yield: address
1149
+ x = Vpim::Vcard::Address.new
1150
+ yield x
1151
+ @card << x.encode
1152
+ self
1153
+ end
1154
+
1155
+ # Add a telephone field, TEL. +tel+ is a Vpim::Vcard::Telephone.
1156
+ #
1157
+ # The block is optional, its only necessary if you want to specify
1158
+ # the optional attributes.
1159
+ def add_tel(number) # :yield: tel
1160
+ x = Vpim::Vcard::Telephone.new(number)
1161
+ if block_given?
1162
+ yield x
1163
+ end
1164
+ @card << x.encode
1165
+ self
1166
+ end
1167
+
1168
+ # Add an email field, EMAIL. +email+ is a Vpim::Vcard::Email.
1169
+ #
1170
+ # The block is optional, its only necessary if you want to specify
1171
+ # the optional attributes.
1172
+ def add_email(email) # :yield: email
1173
+ x = Vpim::Vcard::Email.new(email)
1174
+ if block_given?
1175
+ yield x
1176
+ end
1177
+ @card << x.encode
1178
+ self
1179
+ end
1180
+
1181
+ # Set the nickname field, NICKNAME.
1182
+ #
1183
+ # It can be set to a single String or an Array of String.
1184
+ def nickname=(nickname)
1185
+ delete_if { |l| l.name == 'NICKNAME' }
1186
+
1187
+ @card << Vpim::DirectoryInfo::Field.create( 'NICKNAME', nickname );
1188
+ end
1189
+
1190
+ # Add a birthday field, BDAY.
1191
+ #
1192
+ # +birthday+ must be a time or date object.
1193
+ #
1194
+ # Warning: It may confuse both humans and software if you add multiple
1195
+ # birthdays.
1196
+ def birthday=(birthday)
1197
+ if !birthday.respond_to? :month
1198
+ raise ArgumentError, 'birthday must be a date or time object.'
1199
+ end
1200
+ delete_if { |l| l.name == 'BDAY' }
1201
+ @card << Vpim::DirectoryInfo::Field.create( 'BDAY', birthday );
1202
+ end
1203
+
1204
+ # Add a note field, NOTE. The +note+ String can contain newlines, they
1205
+ # will be escaped.
1206
+ def add_note(note)
1207
+ @card << Vpim::DirectoryInfo::Field.create( 'NOTE', Vpim.encode_text(note) );
1208
+ end
1209
+
1210
+ # Add an instant-messaging/point of presence address field, IMPP. The address
1211
+ # is a URL, with the syntax depending on the protocol.
1212
+ #
1213
+ # Attributes of IMPP are:
1214
+ # - preferred: true - set if this is the preferred address
1215
+ # - location: home, work, mobile - location of address
1216
+ # - purpose: personal,business - purpose of communications
1217
+ #
1218
+ # All attributes are optional, and so is the block.
1219
+ #
1220
+ # The URL syntaxes for the messaging schemes is fairly complicated, so I
1221
+ # don't try and build the URLs here, maybe in the future. This forces
1222
+ # the user to know the URL for their own address, hopefully not too much
1223
+ # of a burden.
1224
+ #
1225
+ # IMPP is defined in draft-jennings-impp-vcard-04.txt. It refers to the
1226
+ # URI scheme of a number of messaging protocols, but doesn't give
1227
+ # references to all of them:
1228
+ # - "xmpp" indicates to use XMPP, draft-saintandre-xmpp-uri-06.txt
1229
+ # - "irc" or "ircs" indicates to use IRC, draft-butcher-irc-url-04.txt
1230
+ # - "sip" indicates to use SIP/SIMPLE, RFC 3261
1231
+ # - "im" or "pres" indicates to use a CPIM or CPP gateway, RFC 3860 and RFC 3859
1232
+ # - "ymsgr" indicates to use yahoo
1233
+ # - "msn" might indicate to use Microsoft messenger
1234
+ # - "aim" indicates to use AOL
1235
+ #
1236
+ def add_impp(url) # :yield: impp
1237
+ params = {}
1238
+
1239
+ if block_given?
1240
+ x = Struct.new( :location, :preferred, :purpose ).new
1241
+
1242
+ yield x
1243
+
1244
+ x[:preferred] = 'PREF' if x[:preferred]
1245
+
1246
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1247
+
1248
+ params['TYPE'] = types if types.first
1249
+ end
1250
+
1251
+ @card << Vpim::DirectoryInfo::Field.create( 'IMPP', url, params)
1252
+ self
1253
+ end
1254
+
1255
+ # Add an X-AIM account name where +xaim+ is an AIM screen name.
1256
+ #
1257
+ # I don't know if this is conventional, or supported by anything other
1258
+ # than AddressBook.app, but an example is:
1259
+ # X-AIM;type=HOME;type=pref:exampleaccount
1260
+ #
1261
+ # Attributes of X-AIM are:
1262
+ # - preferred: true - set if this is the preferred address
1263
+ # - location: home, work, mobile - location of address
1264
+ #
1265
+ # All attributes are optional, and so is the block.
1266
+ def add_x_aim(xaim) # :yield: xaim
1267
+ params = {}
1268
+
1269
+ if block_given?
1270
+ x = Struct.new( :location, :preferred ).new
1271
+
1272
+ yield x
1273
+
1274
+ x[:preferred] = 'PREF' if x[:preferred]
1275
+
1276
+ types = x.to_a.flatten.compact.map { |s| s.downcase }.uniq
1277
+
1278
+ params['TYPE'] = types if types.first
1279
+ end
1280
+
1281
+ @card << Vpim::DirectoryInfo::Field.create( 'X-AIM', xaim, params)
1282
+ self
1283
+ end
1284
+
1285
+
1286
+ # Add a photo field, PHOTO.
1287
+ #
1288
+ # Attributes of PHOTO are:
1289
+ # - image: set to image data to include inline
1290
+ # - link: set to the URL of the image data
1291
+ # - type: string identifying the image type, supposed to be an "IANA registered image format",
1292
+ # or a non-registered image format (usually these start with an x-)
1293
+ #
1294
+ # An error will be raised if neither image or link is set, or if both image
1295
+ # and link is set.
1296
+ #
1297
+ # Setting type is optional for a link image, because either the URL, the
1298
+ # image file extension, or a HTTP Content-Type may specify the type. If
1299
+ # it's not a link, setting type is mandatory, though it can be set to an
1300
+ # empty string, <code>''</code>, if the type is unknown.
1301
+ #
1302
+ # TODO - I'm not sure about this API. I'm thinking maybe it should be
1303
+ # #add_photo(image, type), and that I should detect when the image is a
1304
+ # URL, and make type mandatory if it wasn't a URL.
1305
+ def add_photo # :yield: photo
1306
+ x = Struct.new(:image, :link, :type).new
1307
+ yield x
1308
+ if x[:image] && x[:link]
1309
+ raise Vpim::InvalidEncodingError, 'Image is not allowed to be both inline and a link.'
1310
+ end
1311
+
1312
+ value = x[:image] || x[:link]
1313
+
1314
+ if !value
1315
+ raise Vpim::InvalidEncodingError, 'A image link or inline data must be provided.'
1316
+ end
1317
+
1318
+ params = {}
1319
+
1320
+ # Don't set type to the empty string.
1321
+ params['TYPE'] = x[:type] if( x[:type] && x[:type].length > 0 )
1322
+
1323
+ if x[:link]
1324
+ params['VALUE'] = 'URI'
1325
+ else # it's inline, base-64 encode it
1326
+ params['ENCODING'] = :b64
1327
+ if !x[:type]
1328
+ raise Vpim::InvalidEncodingError, 'Inline image data must have it\'s type set.'
1329
+ end
1330
+ end
1331
+
1332
+ @card << Vpim::DirectoryInfo::Field.create( 'PHOTO', value, params )
1333
+ self
1334
+ end
1335
+
1336
+ # Set the title field, TITLE.
1337
+ #
1338
+ # It can be set to a single String.
1339
+ def title=(title)
1340
+ delete_if { |l| l.name == 'TITLE' }
1341
+
1342
+ @card << Vpim::DirectoryInfo::Field.create( 'TITLE', title );
1343
+ end
1344
+
1345
+ # Set the org field, ORG.
1346
+ #
1347
+ # It can be set to a single String or an Array of String.
1348
+ def org=(org)
1349
+ delete_if { |l| l.name == 'ORG' }
1350
+
1351
+ @card << Vpim::DirectoryInfo::Field.create( 'ORG', org );
1352
+ end
1353
+
1354
+
1355
+ # Add a URL field, URL.
1356
+ def add_url(url)
1357
+ @card << Vpim::DirectoryInfo::Field.create( 'URL', url.to_str );
1358
+ end
1359
+
1360
+ # Add a Field, +field+.
1361
+ def add_field(field)
1362
+ fieldname = field.name.upcase
1363
+ case
1364
+ when [ 'BEGIN', 'END' ].include?(fieldname)
1365
+ raise Vpim::InvalidEncodingError, "Not allowed to manually add #{field.name} to a vCard."
1366
+
1367
+ when [ 'VERSION', 'N', 'FN' ].include?(fieldname)
1368
+ if @card.field(fieldname)
1369
+ raise Vpim::InvalidEncodingError, "Not allowed to add more than one #{fieldname} to a vCard."
1370
+ end
1371
+ @card << field
1372
+
1373
+ else
1374
+ @card << field
1375
+ end
1376
+ end
1377
+
1378
+ # Copy the fields from +card+ into self using #add_field. If a block is
1379
+ # provided, each Field from +card+ is yielded. The block should return a
1380
+ # Field to add, or nil. The Field doesn't have to be the one yielded,
1381
+ # allowing the field to be copied and modified (see Field#copy) before adding, or
1382
+ # not added at all if the block yields nil.
1383
+ #
1384
+ # The vCard fields BEGIN and END aren't copied, and VERSION, N, and FN are copied
1385
+ # only if the card doesn't have them already.
1386
+ def copy(card) # :yields: Field
1387
+ card.each do |field|
1388
+ fieldname = field.name.upcase
1389
+ case
1390
+ when [ 'BEGIN', 'END' ].include?(fieldname)
1391
+ # Never copy these
1392
+
1393
+ when [ 'VERSION', 'N', 'FN' ].include?(fieldname) && @card.field(fieldname)
1394
+ # Copy these only if they don't already exist.
1395
+
1396
+ else
1397
+ if block_given?
1398
+ field = yield field
1399
+ end
1400
+
1401
+ if field
1402
+ add_field(field)
1403
+ end
1404
+ end
1405
+ end
1406
+ end
1407
+
1408
+ # Delete +line+ if block yields true.
1409
+ def delete_if #:yield: line
1410
+ begin
1411
+ @card.delete_if do |line|
1412
+ yield line
1413
+ end
1414
+ rescue NoMethodError
1415
+ # FIXME - this is a hideous hack, allowing a DirectoryInfo to
1416
+ # be passed instead of a Vcard, and for it to almost work. Yuck.
1417
+ end
1418
+ end
1419
+
1420
+ end
1421
+ end
1422
+ end
1423
+