vcard 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/LICENSE +58 -0
- data/README.rdoc +7 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/lib/vcard.rb +34 -0
- data/lib/vcard/attachment.rb +100 -0
- data/lib/vcard/dirinfo.rb +272 -0
- data/lib/vcard/enumerator.rb +30 -0
- data/lib/vcard/field.rb +610 -0
- data/lib/vcard/rfc2425.rb +367 -0
- data/lib/vcard/vcard.rb +1423 -0
- data/test/field_test.rb +152 -0
- data/test/test_helper.rb +10 -0
- data/test/vcard_test.rb +967 -0
- metadata +73 -0
@@ -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
|
+
|
data/lib/vcard/vcard.rb
ADDED
@@ -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
|
+
|