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