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.
- 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
|
+
|