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