vcard 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+